This commit is contained in:
retoor 2025-11-08 23:56:52 +01:00
parent 2ad7401226
commit a6a19b8438
17 changed files with 1153 additions and 388 deletions

View File

@ -10,6 +10,7 @@ import dotenv # Import dotenv
from .routes import setup_routes
from .services.user_service import UserService
from .services.config_service import ConfigService
from .services.file_service import FileService # Import FileService
from .middlewares import user_middleware, error_middleware
from .helpers.env_manager import ensure_env_file_exists, get_or_create_session_secret_key # Import new function
@ -19,6 +20,7 @@ async def setup_services(app: web.Application):
data_path = base_path.parent / "data"
app["user_service"] = UserService(data_path / "users.json")
app["config_service"] = ConfigService(data_path / "config.json")
app["file_service"] = FileService(data_path / "user_files", data_path / "users.json") # Instantiate FileService
# Setup aiojobs scheduler
app["scheduler"] = aiojobs.Scheduler()

View File

@ -1,5 +1,5 @@
from .views.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView
from .views.site import SiteView, OrderView, FileBrowserView
from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView
from .views.admin import get_users, add_user, update_user_quota, delete_user, get_user_details, delete_team
@ -23,7 +23,17 @@ def setup_routes(app):
app.router.add_view("/recent", SiteView, name="recent")
app.router.add_view("/favorites", SiteView, name="favorites")
app.router.add_view("/trash", SiteView, name="trash")
app.router.add_view("/users", SiteView, name="users")
app.router.add_view("/users/add", UserManagementView, name="add_user")
app.router.add_view("/users/{email}/edit", UserManagementView, name="edit_user")
app.router.add_view("/users/{email}/details", UserManagementView, name="user_details")
app.router.add_post("/users/{email}/delete", UserManagementView, name="delete_user_page")
app.router.add_view("/files", FileBrowserView, name="file_browser")
app.router.add_post("/files/new_folder", FileBrowserView, name="new_folder")
app.router.add_post("/files/upload", FileBrowserView, name="upload_file")
app.router.add_get("/files/download/{file_path:.*}", FileBrowserView, name="download_file")
app.router.add_post("/files/share/{file_path:.*}", FileBrowserView, name="share_file")
app.router.add_post("/files/delete/{file_path:.*}", FileBrowserView, name="delete_item")
# Admin API routes for user and team management
app.router.add_get("/api/users", get_users, name="api_get_users")

View File

@ -303,6 +303,63 @@ main {
font-weight: 600;
}
/* Global Button Styles */
.btn-primary,
.btn-outline,
.btn-small,
.btn-danger {
display: inline-block;
padding: 0.6rem 1.2rem;
font-size: 0.9rem;
font-weight: 500;
text-align: center;
text-decoration: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
box-sizing: border-box;
}
.btn-primary {
background-color: var(--btn-primary-bg);
color: var(--btn-primary-text);
border-color: var(--btn-primary-bg);
}
.btn-primary:hover {
background-color: var(--btn-primary-hover-bg);
border-color: var(--btn-primary-hover-bg);
}
.btn-outline {
background-color: transparent;
color: var(--btn-outline-text);
border-color: var(--btn-outline-border);
}
.btn-outline:hover {
background-color: var(--btn-outline-hover-bg);
color: var(--btn-outline-text);
border-color: var(--btn-outline-border);
}
.btn-small {
padding: 0.4rem 0.8rem;
font-size: 0.85rem;
}
.btn-danger {
background-color: #dc3545;
color: white;
border-color: #dc3545;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
}
.error {
color: #D32F2F; /* Red for errors */
background-color: #FFEBEE; /* Light red background */

View File

@ -1,25 +1,24 @@
/* General styles for hero sections on content pages */
.hero-intro {
text-align: center;
padding: 3rem 1rem;
padding: 20px;
background-color: var(--card-background);
margin-bottom: 2rem;
margin-bottom: 20px;
border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color);
}
.hero-headline {
font-size: 2.5rem;
font-size: 1.8rem;
font-weight: 700;
color: var(--text-color);
margin-bottom: 0.8rem;
margin-bottom: 0.5rem;
}
.hero-subheadline {
font-size: 1.2rem;
font-size: 0.95rem;
color: var(--light-text-color);
max-width: 700px;
margin: 0 auto;
max-width: 100%;
margin: 0;
}
/* Pricing Plans Section */
@ -34,8 +33,7 @@
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color);
padding: 2rem;
text-align: center;
padding: 20px;
display: flex;
flex-direction: column;
justify-content: space-between;
@ -53,20 +51,20 @@
}
.plan-card h2 {
font-size: 1.8rem;
font-size: 1.5rem;
color: var(--text-color);
margin-bottom: 1rem;
margin-bottom: 0.8rem;
}
.plan-card .price {
font-size: 3rem;
font-size: 2rem;
font-weight: 700;
color: var(--accent-color);
margin-bottom: 1.5rem;
margin-bottom: 1rem;
}
.plan-card .price span {
font-size: 1rem;
font-size: 0.9rem;
font-weight: 500;
color: var(--light-text-color);
}
@ -74,17 +72,16 @@
.plan-card ul {
list-style: none;
padding: 0;
margin-bottom: 2rem;
margin-bottom: 1.5rem;
flex-grow: 1;
}
.plan-card ul li {
margin-bottom: 0.8rem;
margin-bottom: 0.6rem;
color: var(--light-text-color);
font-size: 1rem;
font-size: 0.9rem;
display: flex;
align-items: center;
justify-content: center;
}
.plan-card ul li i {
@ -95,9 +92,6 @@
.plan-card .btn-primary,
.plan-card .btn-outline {
width: 100%;
padding: 0.8rem 1.5rem;
font-size: 1.1rem;
border-radius: 5px;
}
/* Security Features Section */
@ -112,8 +106,7 @@
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color);
padding: 2rem;
text-align: center;
padding: 20px;
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
@ -123,21 +116,21 @@
}
.feature-card i {
font-size: 3rem; /* Placeholder for icon size */
font-size: 2rem;
color: var(--accent-color);
margin-bottom: 1rem;
margin-bottom: 0.8rem;
display: block;
}
.feature-card h2 {
font-size: 1.5rem;
font-size: 1.2rem;
color: var(--text-color);
margin-bottom: 0.8rem;
margin-bottom: 0.6rem;
}
.feature-card p {
color: var(--light-text-color);
font-size: 1rem;
font-size: 0.9rem;
}
/* FAQ Section */
@ -145,14 +138,14 @@
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color);
padding: 2rem;
margin-bottom: 2rem;
padding: 20px;
margin-bottom: 20px;
}
.faq-section h2 {
text-align: center;
margin-bottom: 2rem;
margin-bottom: 20px;
color: var(--text-color);
font-size: 1.5rem;
}
.faq-item {
@ -168,77 +161,71 @@
}
.faq-item h3 {
font-size: 1.2rem;
font-size: 1.1rem;
color: var(--text-color);
margin-bottom: 0.5rem;
}
.faq-item p {
color: var(--light-text-color);
font-size: 1rem;
font-size: 0.9rem;
}
/* CTA Section */
.cta-section {
text-align: center;
padding: 3rem 1rem;
padding: 20px;
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 12px var(--shadow-color);
}
.cta-section h2 {
font-size: 2rem;
font-size: 1.5rem;
color: var(--text-color);
margin-bottom: 1rem;
margin-bottom: 0.8rem;
}
.cta-section p {
font-size: 1.1rem;
font-size: 0.95rem;
color: var(--light-text-color);
max-width: 700px;
margin: 0 auto 1.5rem auto;
}
.cta-section .btn-primary {
font-size: 1.2rem;
padding: 1rem 2rem;
max-width: 100%;
margin: 0 0 1rem 0;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.hero-headline {
font-size: 2rem;
font-size: 1.5rem;
}
.hero-subheadline {
font-size: 1rem;
font-size: 0.9rem;
}
.pricing-plans,
.security-features {
grid-template-columns: 1fr; /* Stack cards on smaller screens */
grid-template-columns: 1fr;
}
.plan-card,
.feature-card {
padding: 1.5rem;
padding: 15px;
}
.plan-card h2,
.feature-card h2 {
font-size: 1.6rem;
font-size: 1.2rem;
}
.plan-card .price {
font-size: 2.5rem;
font-size: 1.5rem;
}
.cta-section h2 {
font-size: 1.8rem;
font-size: 1.3rem;
}
.cta-section p {
font-size: 1rem;
font-size: 0.9rem;
}
}

View File

@ -1,50 +1,238 @@
/* retoors/static/css/components/file_browser.css */
.file-browser-section {
padding: 2rem;
max-width: 800px;
margin: 0 auto;
background-color: var(--color-background-light);
border-radius: var(--border-radius);
box-shadow: var(--shadow-elevation-low);
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
}
.file-browser-section h1 {
color: var(--color-primary);
text-align: center;
margin-bottom: 1.5rem;
.modal-content {
background-color: var(--card-background);
margin: 10% auto;
padding: 30px;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.file-list ul {
list-style: none;
padding: 0;
.modal-content h3 {
margin-top: 0;
margin-bottom: 20px;
color: var(--text-color);
}
.file-list li {
background-color: var(--color-surface);
margin-bottom: 0.5rem;
padding: 0.8rem 1rem;
border-radius: var(--border-radius-small);
.close {
color: var(--light-text-color);
float: right;
font-size: 28px;
font-weight: bold;
line-height: 20px;
cursor: pointer;
}
.close:hover,
.close:focus {
color: var(--text-color);
}
.form-input {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 1rem;
background-color: var(--background-color);
color: var(--text-color);
}
.form-input:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(0, 188, 212, 0.2);
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 20px;
}
.btn-small {
padding: 0.4rem 0.8rem;
font-size: 0.85rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--background-color);
color: var(--text-color);
cursor: pointer;
transition: all 0.2s ease;
}
.btn-small:hover {
background-color: var(--accent-color);
color: white;
border-color: var(--accent-color);
}
.btn-danger {
background-color: #dc3545;
color: white;
border-color: #dc3545;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
}
.action-buttons {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.file-info {
padding: 10px;
margin-bottom: 10px;
background-color: var(--background-color);
border-radius: 4px;
font-size: 0.9rem;
color: var(--text-color);
}
.alert {
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
font-size: 0.95rem;
}
.alert-success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.alert-error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
#share-link-input {
margin-bottom: 10px;
}
#share-loading {
padding: 20px;
text-align: center;
color: var(--light-text-color);
}
.storage-gauge {
width: 100%;
height: 8px;
background-color: var(--border-color);
border-radius: 4px;
overflow: hidden;
margin-bottom: 10px;
}
.storage-gauge-bar {
height: 100%;
background: linear-gradient(90deg, var(--accent-color) 0%, #00d4ff 100%);
transition: width 0.3s ease;
}
.storage-info {
display: flex;
align-items: center;
justify-content: space-between;
border: 1px solid var(--color-border);
font-size: 0.85rem;
color: var(--light-text-color);
}
.file-list li a {
color: var(--color-text);
.file-list-table table {
width: 100%;
border-collapse: collapse;
}
.file-list-table th {
padding: 12px 15px;
text-align: left;
background-color: var(--background-color);
color: var(--text-color);
font-weight: 600;
font-size: 0.9rem;
border-bottom: 2px solid var(--border-color);
}
.file-list-table td {
padding: 12px 15px;
border-bottom: 1px solid var(--border-color);
color: var(--text-color);
}
.file-list-table tr:hover {
background-color: var(--background-color);
}
.file-list-table tr:last-child td {
border-bottom: none;
}
.file-icon {
width: 20px;
height: 20px;
vertical-align: middle;
margin-right: 8px;
}
.file-list-table a {
color: var(--accent-color);
text-decoration: none;
flex-grow: 1;
font-weight: var(--font-weight-medium);
}
.file-list li a:hover {
color: var(--color-primary-dark);
.file-list-table a:hover {
text-decoration: underline;
}
.file-list p {
text-align: center;
color: var(--color-text-secondary);
font-style: italic;
@media (max-width: 768px) {
.modal-content {
width: 95%;
margin: 20% auto;
padding: 20px;
}
.action-buttons {
flex-direction: column;
}
.btn-small {
width: 100%;
}
.file-list-table {
font-size: 0.85rem;
}
.file-list-table th,
.file-list-table td {
padding: 8px 10px;
}
.dashboard-actions {
flex-wrap: wrap;
}
.dashboard-actions button {
flex: 1 1 calc(50% - 5px);
min-width: 120px;
}
}

View File

@ -1,30 +1,27 @@
/* Styles for the Quota Management / Admin Dashboard (order.html) */
.order-management-layout {
padding: 40px 20px;
max-width: 1200px;
margin: 0 auto;
padding: 0;
}
.quota-overview-card {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 6px 20px var(--shadow-color);
padding: 30px;
margin-bottom: 40px;
text-align: center;
box-shadow: 0 4px 12px var(--shadow-color);
padding: 20px;
margin-bottom: 30px;
}
.quota-overview-card h2 {
font-size: 2.5rem;
font-size: 1.8rem;
color: var(--text-color);
margin-bottom: 10px;
}
.quota-overview-card .subtitle {
font-size: 1.1rem;
font-size: 0.95rem;
color: var(--light-text-color);
margin-bottom: 30px;
margin-bottom: 20px;
}
.overview-grid {
@ -35,17 +32,16 @@
}
.total-storage-chart {
text-align: center;
padding: 20px;
border-radius: 8px;
background-color: var(--background-color); /* Lighter background for this section */
background-color: var(--background-color);
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.total-storage-chart h3 {
font-size: 1.3rem;
font-size: 1.1rem;
color: var(--text-color);
margin-bottom: 20px;
margin-bottom: 15px;
}
.donut-chart-container {
@ -96,15 +92,13 @@
background-color: var(--background-color);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
padding: 30px;
text-align: left;
padding: 20px;
}
.order-form-card h3 {
font-size: 1.3rem;
font-size: 1.1rem;
color: var(--text-color);
margin-bottom: 20px;
text-align: center;
margin-bottom: 15px;
}
.order-form {
@ -119,24 +113,21 @@
}
.order-form .price-display {
font-size: 1.4rem;
font-weight: 700;
font-size: 1rem;
font-weight: 600;
color: var(--accent-color);
text-align: center;
margin-top: 10px;
}
.order-form .btn-primary {
width: 100%;
padding: 12px;
font-size: 1.1rem;
}
.user-quotas-section {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 6px 20px var(--shadow-color);
padding: 30px;
box-shadow: 0 4px 12px var(--shadow-color);
padding: 20px;
}
.user-quotas-header {
@ -149,16 +140,11 @@
}
.user-quotas-header h2 {
font-size: 2rem;
font-size: 1.8rem;
color: var(--text-color);
margin: 0;
}
.user-quotas-header .btn-primary {
padding: 0.7rem 1.5rem;
font-size: 0.95rem;
}
.user-quota-list {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
@ -176,7 +162,7 @@
}
.user-quota-item .user-info h4 {
font-size: 1.2rem;
font-size: 1.1rem;
color: var(--text-color);
margin-bottom: 5px;
}
@ -199,16 +185,14 @@
.user-quota-item .quota-actions {
display: flex;
flex-wrap: wrap; /* Allow buttons to wrap */
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.user-quota-item .quota-actions .btn-outline {
padding: 0.5rem 1rem;
font-size: 0.85rem;
flex-grow: 1; /* Allow buttons to grow and fill space */
min-width: 100px; /* Ensure buttons don't get too small */
flex-grow: 1;
min-width: 100px;
}
/* Small storage gauge for user items */
@ -259,8 +243,7 @@
color: var(--text-color);
margin-top: 0;
margin-bottom: 20px;
font-size: 1.8rem;
text-align: center;
font-size: 1.2rem;
}
.modal-content .form-group {
@ -289,15 +272,12 @@
.modal-content .btn-primary {
width: 100%;
padding: 12px;
font-size: 1.1rem;
margin-top: 20px;
}
.modal-content .error {
color: var(--error-color);
margin-top: 10px;
text-align: center;
}
.close-button {
@ -344,17 +324,14 @@
}
@media (max-width: 768px) {
.order-management-layout {
padding: 20px 15px;
}
.quota-overview-card h2 {
font-size: 2rem;
font-size: 1.5rem;
}
.quota-overview-card .subtitle {
font-size: 1rem;
font-size: 0.9rem;
}
.user-quotas-header h2 {
font-size: 1.8rem;
font-size: 1.5rem;
}
.user-quota-list {
grid-template-columns: 1fr;
@ -367,10 +344,10 @@
@media (max-width: 480px) {
.quota-overview-card {
padding: 20px;
padding: 15px;
}
.quota-overview-card h2 {
font-size: 1.8rem;
font-size: 1.2rem;
}
.donut-chart-container {
width: 150px;
@ -381,19 +358,19 @@
height: 110px;
}
.donut-chart-text {
font-size: 1.2rem;
font-size: 0.95rem;
}
.order-form-card {
padding: 20px;
padding: 15px;
}
.user-quotas-section {
padding: 20px;
padding: 15px;
}
.user-quotas-header h2 {
font-size: 1.5rem;
font-size: 1.1rem;
}
.user-quota-item .quota-actions .btn-outline {
flex-grow: unset; /* Reset flex-grow for smaller screens if needed */
width: 100%; /* Make buttons full width */
flex-grow: unset;
width: 100%;
}
}

View File

@ -1,71 +1,60 @@
/* Styles for the Support Page (support.html) */
.support-hero {
text-align: center;
padding: 40px 20px;
margin-bottom: 60px;
padding: 0;
margin-bottom: 30px;
}
.support-hero h1 {
font-size: 3rem;
font-size: 1.8rem;
color: var(--text-color);
margin-bottom: 1rem;
margin-bottom: 0.5rem;
}
.support-hero p {
font-size: 1.2rem;
font-size: 0.95rem;
color: var(--light-text-color);
max-width: 800px;
margin: 0 auto 30px auto;
max-width: 100%;
margin: 0 0 20px 0;
}
.search-support {
display: flex;
justify-content: center;
gap: 10px;
max-width: 600px;
margin: 0 auto;
margin-bottom: 30px;
}
.search-support .search-input {
flex-grow: 1;
padding: 12px 20px;
padding: 10px 15px;
border: 1px solid var(--border-color);
border-radius: 5px;
font-size: 1rem;
border-radius: 4px;
font-size: 0.9rem;
}
.search-support .btn-primary {
padding: 12px 25px;
font-size: 1rem;
}
.support-categories {
text-align: center;
margin-bottom: 60px;
margin-bottom: 40px;
}
.support-categories h2 {
font-size: 2.5rem;
font-size: 1.5rem;
color: var(--text-color);
margin-bottom: 40px;
margin-bottom: 20px;
}
.category-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 30px;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
gap: 20px;
}
.category-card {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 15px var(--shadow-color);
padding: 30px;
text-align: center;
box-shadow: 0 4px 12px var(--shadow-color);
padding: 20px;
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
@ -75,22 +64,22 @@
}
.category-card img.icon {
width: 70px;
height: 70px;
margin-bottom: 20px;
width: 50px;
height: 50px;
margin-bottom: 15px;
}
.category-card h3 {
font-size: 1.5rem;
font-size: 1.1rem;
color: var(--text-color);
margin-bottom: 10px;
}
.category-card p {
font-size: 0.95rem;
font-size: 0.9rem;
color: var(--light-text-color);
line-height: 1.5;
margin-bottom: 20px;
line-height: 1.6;
margin-bottom: 15px;
}
.category-card .btn-link {
@ -104,31 +93,26 @@
}
.contact-options {
text-align: center;
margin-bottom: 60px;
margin-bottom: 40px;
}
.contact-options h2 {
font-size: 2.5rem;
font-size: 1.5rem;
color: var(--text-color);
margin-bottom: 40px;
margin-bottom: 20px;
}
.contact-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
gap: 20px;
}
.contact-card {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 15px var(--shadow-color);
padding: 30px;
text-align: center;
box-shadow: 0 4px 12px var(--shadow-color);
padding: 20px;
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
@ -138,27 +122,22 @@
}
.contact-card img.icon {
width: 70px;
height: 70px;
margin-bottom: 20px;
width: 50px;
height: 50px;
margin-bottom: 15px;
}
.contact-card h3 {
font-size: 1.5rem;
font-size: 1.1rem;
color: var(--text-color);
margin-bottom: 10px;
}
.contact-card p {
font-size: 0.95rem;
font-size: 0.9rem;
color: var(--light-text-color);
line-height: 1.5;
margin-bottom: 20px;
}
.contact-card .btn-primary {
padding: 10px 20px;
font-size: 1rem;
line-height: 1.6;
margin-bottom: 15px;
}
.contact-card .phone-hours {
@ -170,10 +149,10 @@
/* Responsive adjustments */
@media (max-width: 768px) {
.support-hero h1, .support-categories h2, .contact-options h2 {
font-size: 2.5rem;
font-size: 1.5rem;
}
.support-hero p {
font-size: 1rem;
font-size: 0.9rem;
}
.search-support {
flex-direction: column;
@ -188,9 +167,9 @@
@media (max-width: 480px) {
.support-hero h1, .support-categories h2, .contact-options h2 {
font-size: 2rem;
font-size: 1.2rem;
}
.category-card, .contact-card {
padding: 20px;
padding: 15px;
}
}

View File

@ -11,7 +11,7 @@
<aside class="dashboard-sidebar">
<div class="sidebar-menu">
<ul>
<li><a href="/dashboard" class="active"><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li>
<li><a href="/files" class="active"><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li>
<li><a href="/shared"><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li>
<li><a href="/recent"><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li>
<li><a href="/favorites"><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li>

View File

@ -1,15 +1,12 @@
{% extends "layouts/base.html" %}
{% extends "layouts/dashboard.html" %}
{% block title %}Favorites - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Favorites</h1>
{% block page_title %}Favorites{% endblock %}
{% block dashboard_content %}
<div class="content-section">
<p>Your favorite files and folders will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
<p>This feature is coming soon.</p>
</div>
{% endblock %}

View File

@ -1,24 +1,295 @@
{% extends "layouts/base.html" %}
{% block title %}File Browser{% endblock %}
{% block head %}
{{ super() }}
{% extends "layouts/dashboard.html" %}
{% block title %}My Files - Retoor's Cloud Solutions{% endblock %}
{% block dashboard_head %}
<link rel="stylesheet" href="/static/css/components/file_browser.css">
{% endblock %}
{% block content %}
<main>
<section class="file-browser-section">
<h1>File Browser</h1>
<div class="file-list">
{% if files %}
<ul>
{% for file in files %}
<li><a href="/files/{{ file }}" target="_blank">{{ file }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No files found in the project directory.</p>
{% endif %}
</div>
</section>
</main>
{% block page_title %}My Files{% endblock %}
{% block dashboard_actions %}
<button class="btn-primary" onclick="showNewFolderModal()">+ New</button>
<button class="btn-outline" onclick="showUploadModal()">Upload</button>
<button class="btn-outline" onclick="downloadSelected()" id="download-btn" disabled>Download</button>
<button class="btn-outline" onclick="shareSelected()" id="share-btn" disabled>Share</button>
<button class="btn-outline" onclick="deleteSelected()" id="delete-btn" disabled>Delete</button>
{% endblock %}
{% block dashboard_content %}
{% if success_message %}
<div class="alert alert-success">
{{ success_message }}
</div>
{% endif %}
{% if error_message %}
<div class="alert alert-error">
{{ error_message }}
</div>
{% endif %}
<input type="text" class="file-search-bar" placeholder="Search your files..." id="search-bar">
<div class="file-list-table">
<table>
<thead>
<tr>
<th><input type="checkbox" id="select-all"></th>
<th>Name</th>
<th>Owner</th>
<th>Last Modified</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="file-list-body">
{% if files %}
{% for item in files %}
<tr data-path="{{ item.path }}" data-is-dir="{{ item.is_dir }}">
<td><input type="checkbox" class="file-checkbox" data-path="{{ item.path }}" data-is-dir="{{ item.is_dir }}"></td>
<td>
{% if item.is_dir %}
<img src="/static/images/icon-families.svg" alt="Folder Icon" class="file-icon">
<a href="/files?path={{ item.path }}">{{ item.name }}</a>
{% else %}
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
{{ item.name }}
{% endif %}
</td>
<td>{{ user.email }}</td>
<td>{{ item.last_modified[:10] }}</td>
<td>
{% if item.is_dir %}
--
{% else %}
{{ (item.size / 1024 / 1024)|round(2) }} MB
{% endif %}
</td>
<td>
<div class="action-buttons">
{% if not item.is_dir %}
<button class="btn-small" onclick="downloadFile('{{ item.path }}')">Download</button>
{% endif %}
<button class="btn-small" onclick="shareFile('{{ item.path }}', '{{ item.name }}')">Share</button>
<button class="btn-small btn-danger" onclick="deleteFile('{{ item.path }}', '{{ item.name }}')">Delete</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="6" style="text-align: center; padding: 40px;">
<p>No files found in this directory.</p>
<button class="btn-primary" onclick="showNewFolderModal()" style="margin-top: 10px;">Create your first folder</button>
<button class="btn-outline" onclick="showUploadModal()" style="margin-top: 10px;">Upload a file</button>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
<div id="new-folder-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('new-folder-modal')">&times;</span>
<h3>Create New Folder</h3>
<form action="/files/new_folder" method="post">
<input type="text" name="folder_name" placeholder="Folder name" required class="form-input">
<div class="modal-actions">
<button type="submit" class="btn-primary">Create</button>
<button type="button" class="btn-outline" onclick="closeModal('new-folder-modal')">Cancel</button>
</div>
</form>
</div>
</div>
<div id="upload-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('upload-modal')">&times;</span>
<h3>Upload File</h3>
<form action="/files/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" required class="form-input" id="file-input">
<div class="file-info" id="file-info"></div>
<div class="modal-actions">
<button type="submit" class="btn-primary">Upload</button>
<button type="button" class="btn-outline" onclick="closeModal('upload-modal')">Cancel</button>
</div>
</form>
</div>
</div>
<div id="share-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('share-modal')">&times;</span>
<h3>Share File</h3>
<p id="share-file-name"></p>
<div id="share-link-container" style="display: none;">
<input type="text" id="share-link-input" readonly class="form-input">
<button class="btn-primary" onclick="copyShareLink()">Copy Link</button>
</div>
<div id="share-loading">Generating share link...</div>
<div class="modal-actions">
<button type="button" class="btn-outline" onclick="closeModal('share-modal')">Close</button>
</div>
</div>
</div>
<div id="delete-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('delete-modal')">&times;</span>
<h3>Confirm Delete</h3>
<p id="delete-message"></p>
<form id="delete-form" method="post">
<div class="modal-actions">
<button type="submit" class="btn-danger">Delete</button>
<button type="button" class="btn-outline" onclick="closeModal('delete-modal')">Cancel</button>
</div>
</form>
</div>
</div>
<script>
function showNewFolderModal() {
document.getElementById('new-folder-modal').style.display = 'block';
}
function showUploadModal() {
document.getElementById('upload-modal').style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
document.getElementById('file-input').addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const size = (file.size / 1024 / 1024).toFixed(2);
document.getElementById('file-info').innerHTML = `Selected: ${file.name} (${size} MB)`;
}
});
function downloadFile(path) {
window.location.href = `/files/download/${path}`;
}
async function shareFile(path, name) {
const modal = document.getElementById('share-modal');
const linkContainer = document.getElementById('share-link-container');
const loading = document.getElementById('share-loading');
document.getElementById('share-file-name').textContent = `Sharing: ${name}`;
linkContainer.style.display = 'none';
loading.style.display = 'block';
modal.style.display = 'block';
try {
const response = await fetch(`/files/share/${path}`, {
method: 'POST'
});
const data = await response.json();
if (data.share_link) {
document.getElementById('share-link-input').value = data.share_link;
linkContainer.style.display = 'block';
loading.style.display = 'none';
}
} catch (error) {
loading.textContent = 'Error generating share link';
}
}
function copyShareLink() {
const input = document.getElementById('share-link-input');
input.select();
document.execCommand('copy');
alert('Share link copied to clipboard');
}
function deleteFile(path, name) {
document.getElementById('delete-message').textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
document.getElementById('delete-form').action = `/files/delete/${path}`;
document.getElementById('delete-modal').style.display = 'block';
}
document.getElementById('select-all').addEventListener('change', function(e) {
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => cb.checked = e.target.checked);
updateActionButtons();
});
document.querySelectorAll('.file-checkbox').forEach(cb => {
cb.addEventListener('change', updateActionButtons);
});
function updateActionButtons() {
const checked = document.querySelectorAll('.file-checkbox:checked');
const downloadBtn = document.getElementById('download-btn');
const shareBtn = document.getElementById('share-btn');
const deleteBtn = document.getElementById('delete-btn');
const hasSelection = checked.length > 0;
const hasFiles = Array.from(checked).some(cb => cb.dataset.isDir === 'False');
downloadBtn.disabled = !hasFiles;
shareBtn.disabled = !hasSelection;
deleteBtn.disabled = !hasSelection;
}
function downloadSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 1 && checked[0].dataset.isDir === 'False') {
downloadFile(checked[0].dataset.path);
}
}
function shareSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length === 1) {
const path = checked[0].dataset.path;
const name = checked[0].closest('tr').querySelector('td:nth-child(2)').textContent.trim();
shareFile(path, name);
}
}
function deleteSelected() {
const checked = document.querySelectorAll('.file-checkbox:checked');
if (checked.length > 0) {
const paths = Array.from(checked).map(cb => cb.dataset.path);
const names = Array.from(checked).map(cb =>
cb.closest('tr').querySelector('td:nth-child(2)').textContent.trim()
);
if (checked.length === 1) {
deleteFile(paths[0], names[0]);
} else {
document.getElementById('delete-message').textContent =
`Are you sure you want to delete ${checked.length} items? This action cannot be undone.`;
document.getElementById('delete-modal').style.display = 'block';
}
}
}
document.getElementById('search-bar').addEventListener('input', function(e) {
const searchTerm = e.target.value.toLowerCase();
const rows = document.querySelectorAll('#file-list-body tr');
rows.forEach(row => {
const name = row.querySelector('td:nth-child(2)')?.textContent.toLowerCase();
if (name && name.includes(searchTerm)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
});
</script>
{% endblock %}

View File

@ -1,13 +1,15 @@
{% extends "layouts/base.html" %}
{% extends "layouts/dashboard.html" %}
{% block title %}Quota Management - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{% block dashboard_head %}
<link rel="stylesheet" href="/static/css/components/order.css">
{% endblock %}
{% block content %}
<main class="order-management-layout">
{% block page_title %}Manage Quota{% endblock %}
{% block dashboard_content %}
<div class="order-management-layout">
<section class="quota-overview-card">
<h2>Optimize Your Team's Storage</h2>
<p class="subtitle">Flexible cloud monitoring for teams with ease.</p>
@ -46,75 +48,7 @@
</div>
</section>
<section class="user-quotas-section">
<div class="user-quotas-header">
<h2>User & Department Quotas</h2>
<button class="btn-primary" id="add-new-user-btn">+ Add New User</button>
</div>
<div class="user-quota-list" id="user-quota-list">
{# User quota items will be dynamically loaded here by JavaScript #}
</div>
</section>
</div>
{# Add New User Modal #}
<div id="add-user-modal" class="modal">
<div class="modal-content">
<span class="close-button">&times;</span>
<h3>Add New User</h3>
<form id="add-user-form">
<div class="form-group">
<label for="new-user-full-name">Full Name</label>
<input type="text" id="new-user-full-name" name="full_name" required>
</div>
<div class="form-group">
<label for="new-user-email">Email</label>
<input type="email" id="new-user-email" name="email" required>
</div>
<div class="form-group">
<label for="new-user-password">Password</label>
<input type="password" id="new-user-password" name="password" required>
</div>
<div class="form-group">
<label for="new-user-confirm-password">Confirm Password</label>
<input type="password" id="new-user-confirm-password" name="confirm_password" required>
</div>
<button type="submit" class="btn-primary">Add User</button>
<p id="add-user-message" class="error"></p>
</form>
</div>
</div>
{# Edit Quota Modal #}
<div id="edit-quota-modal" class="modal">
<div class="modal-content">
<span class="close-button">&times;</span>
<h3>Edit User Quota</h3>
<form id="edit-quota-form">
<input type="hidden" id="edit-quota-user-email">
<div class="form-group">
<label for="edit-quota-amount">Storage Quota (GB)</label>
<input type="number" id="edit-quota-amount" name="new_quota_gb" min="1" required>
</div>
<button type="submit" class="btn-primary">Update Quota</button>
<p id="edit-quota-message" class="error"></p>
</form>
</div>
</div>
{# View Details Modal #}
<div id="view-details-modal" class="modal">
<div class="modal-content">
<span class="close-button">&times;</span>
<h3>User Details</h3>
<div id="user-details-content">
{# User details will be loaded here #}
</div>
</div>
</div>
</main>
{% endblock %}
{% block scripts %}
<script src="/static/js/components/order.js"></script>
<script src="/static/js/components/order_form.js"></script>
{% endblock %}

View File

@ -1,15 +1,12 @@
{% extends "layouts/base.html" %}
{% extends "layouts/dashboard.html" %}
{% block title %}Recent Files - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Recent Files</h1>
{% block page_title %}Recent Files{% endblock %}
{% block dashboard_content %}
<div class="content-section">
<p>Your recently accessed files will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
<p>This feature is coming soon.</p>
</div>
{% endblock %}

View File

@ -1,15 +1,12 @@
{% extends "layouts/base.html" %}
{% extends "layouts/dashboard.html" %}
{% block title %}Shared with me - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Shared with me</h1>
{% block page_title %}Shared with me{% endblock %}
{% block dashboard_content %}
<div class="content-section">
<p>Files and folders that have been shared with you will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
<p>This feature is coming soon.</p>
</div>
{% endblock %}

View File

@ -1,13 +1,14 @@
{% extends "layouts/base.html" %}
{% extends "layouts/dashboard.html" %}
{% block title %}Support - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{% block dashboard_head %}
<link rel="stylesheet" href="/static/css/components/support.css">
{% endblock %}
{% block content %}
<main>
{% block page_title %}Help & Support{% endblock %}
{% block dashboard_content %}
<section class="support-hero">
<h1>We're Here to Help You Succeed</h1>
<p>Find answers to your questions, troubleshoot issues, or contact our support team for personalized assistance.</p>
@ -70,5 +71,4 @@
</div>
</div>
</section>
</main>
{% endblock %}

View File

@ -1,15 +1,12 @@
{% extends "layouts/base.html" %}
{% extends "layouts/dashboard.html" %}
{% block title %}Trash - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %}
{% block content %}
<main>
<section class="content-section">
<h1>Trash</h1>
{% block page_title %}Trash{% endblock %}
{% block dashboard_content %}
<div class="content-section">
<p>Files and folders you have deleted will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
<p>This feature is coming soon.</p>
</div>
{% endblock %}

View File

@ -2,11 +2,13 @@ from aiohttp import web
import aiohttp_jinja2
import os
from pathlib import Path
from aiohttp.web_response import json_response
from ..helpers.auth import login_required
from .auth import CustomPydanticView
PROJECT_DIR = Path(__file__).parent.parent.parent / "project"
# PROJECT_DIR is no longer directly used for user files, as FileService manages them
# PROJECT_DIR = Path(__file__).parent.parent.parent / "project"
class SiteView(web.View):
@ -35,17 +37,15 @@ class SiteView(web.View):
return await self.favorites()
elif self.request.path == "/trash":
return await self.trash()
elif self.request.path == "/users":
return await self.users()
return aiohttp_jinja2.render_template(
"pages/index.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
@login_required
async def dashboard(self):
return aiohttp_jinja2.render_template(
"pages/dashboard.html",
self.request,
{"user": self.request["user"], "request": self.request, "errors": {}},
)
return web.HTTPFound(self.request.app.router["file_browser"].url_for())
async def solutions(self):
return aiohttp_jinja2.render_template(
@ -62,9 +62,10 @@ class SiteView(web.View):
"pages/security.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
@login_required
async def support(self):
return aiohttp_jinja2.render_template(
"pages/support.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
"pages/support.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "support"}
)
async def use_cases(self):
@ -82,42 +83,201 @@ class SiteView(web.View):
"pages/privacy.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
)
@login_required
async def shared(self):
return aiohttp_jinja2.render_template(
"pages/shared.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
"pages/shared.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "shared"}
)
@login_required
async def recent(self):
return aiohttp_jinja2.render_template(
"pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
"pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "recent"}
)
@login_required
async def favorites(self):
return aiohttp_jinja2.render_template(
"pages/favorites.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
"pages/favorites.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "favorites"}
)
@login_required
async def trash(self):
return aiohttp_jinja2.render_template(
"pages/trash.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
"pages/trash.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "trash"}
)
@login_required
async def users(self):
success_message = self.request.query.get("success")
error_message = self.request.query.get("error")
return aiohttp_jinja2.render_template(
"pages/users.html", self.request, {
"request": self.request,
"errors": {},
"user": self.request["user"],
"active_page": "users",
"success_message": success_message,
"error_message": error_message
}
)
class FileBrowserView(web.View):
@login_required
async def get(self):
files = []
if PROJECT_DIR.is_dir():
for item in os.listdir(PROJECT_DIR):
item_path = PROJECT_DIR / item
if item_path.is_file():
files.append(item)
if self.request.match_info.get("file_path") is not None:
return await self.get_download_file()
user_email = self.request["user"]["email"]
file_service = self.request.app["file_service"]
path = self.request.query.get("path", "")
files = await file_service.list_files(user_email, path)
success_message = self.request.query.get("success")
error_message = self.request.query.get("error")
return aiohttp_jinja2.render_template(
"pages/file_browser.html",
self.request,
{"request": self.request, "files": files, "user": self.request.get("user")},
{
"request": self.request,
"files": files,
"user": self.request["user"],
"current_path": path,
"success_message": success_message,
"error_message": error_message,
"active_page": "files",
},
)
@login_required
async def post(self):
user_email = self.request["user"]["email"]
file_service = self.request.app["file_service"]
route_name = self.request.match_info.route.name
if route_name == "new_folder":
data = await self.request.post()
folder_name = data.get("folder_name")
if not folder_name:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="Folder name is required"
)
)
success = await file_service.create_folder(user_email, folder_name)
if success:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
success=f"Folder '{folder_name}' created successfully"
)
)
else:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error=f"Folder '{folder_name}' already exists or could not be created"
)
)
elif route_name == "upload_file":
try:
reader = await self.request.multipart()
field = await reader.next()
if not field or field.name != "file":
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="No file selected for upload"
)
)
filename = field.filename
if not filename:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="Filename is required"
)
)
content = await field.read()
success = await file_service.upload_file(user_email, filename, content)
if success:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
success=f"File '{filename}' uploaded successfully"
)
)
else:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error=f"Failed to upload file '{filename}'"
)
)
except Exception as e:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error=f"Upload error: {str(e)}"
)
)
elif route_name == "share_file":
file_path = self.request.match_info.get("file_path")
if not file_path:
return json_response({"error": "File path is required for sharing"}, status=400)
share_id = await file_service.generate_share_link(user_email, file_path)
if share_id:
share_link = f"{self.request.scheme}://{self.request.host}/shared_file/{share_id}"
return json_response({"share_link": share_link})
else:
return json_response({"error": "Failed to generate share link"}, status=500)
elif route_name == "delete_item":
item_path = self.request.match_info.get("file_path")
if not item_path:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="Item path is required for deletion"
)
)
success = await file_service.delete_item(user_email, item_path)
if success:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
success=f"Item deleted successfully"
)
)
else:
return web.HTTPFound(
self.request.app.router["file_browser"].url_for().with_query(
error="Failed to delete item - it may not exist"
)
)
return web.HTTPBadRequest(text="Unknown file action")
@login_required
async def get_download_file(self):
user_email = self.request["user"]["email"]
file_service = self.request.app["file_service"]
file_path = self.request.match_info.get("file_path")
if not file_path:
return web.HTTPBadRequest(text="File path is required for download")
result = await file_service.download_file(user_email, file_path)
if result:
content, filename = result
response = web.Response(body=content)
response.headers["Content-Disposition"] = f"attachment; filename={filename}"
response.headers["Content-Type"] = "application/octet-stream"
return response
else:
return web.HTTPNotFound(text="File not found")
class OrderView(CustomPydanticView):
template_name = "pages/order.html"
@ -130,22 +290,236 @@ class OrderView(CustomPydanticView):
self.template_name, self.request, {
"request": self.request,
"errors": {},
"user": self.request.get("user"),
"price_per_gb": price_per_gb
"user": self.request["user"],
"price_per_gb": price_per_gb,
"active_page": "order"
}
)
@login_required
async def post(self):
# The quota update for the main user is now handled via AJAX in order.js
# This POST method will simply re-render the page.
config_service = self.request.app["config_service"]
price_per_gb = config_service.get_price_per_gb()
return aiohttp_jinja2.render_template(
self.template_name, self.request, {
"request": self.request,
"errors": {},
"user": self.request.get("user"),
"price_per_gb": price_per_gb
"user": self.request["user"],
"price_per_gb": price_per_gb,
"active_page": "order"
}
)
class UserManagementView(web.View):
@login_required
async def get(self):
route_name = self.request.match_info.route.name
if route_name == "add_user":
return await self.add_user_page()
elif route_name == "edit_user":
return await self.edit_user_page()
elif route_name == "user_details":
return await self.user_details_page()
return web.HTTPNotFound()
@login_required
async def post(self):
route_name = self.request.match_info.route.name
if route_name == "add_user":
return await self.add_user_submit()
elif route_name == "edit_user":
return await self.edit_user_submit()
elif route_name == "delete_user_page":
return await self.delete_user_submit()
return web.HTTPNotFound()
async def add_user_page(self):
return aiohttp_jinja2.render_template(
"pages/add_user.html",
self.request,
{
"request": self.request,
"user": self.request["user"],
"errors": {},
"form_data": {},
"active_page": "users"
}
)
async def add_user_submit(self):
data = await self.request.post()
full_name = data.get("full_name", "").strip()
email = data.get("email", "").strip()
password = data.get("password", "")
confirm_password = data.get("confirm_password", "")
storage_quota_gb = data.get("storage_quota_gb", "10")
errors = {}
if not full_name:
errors["full_name"] = "Full name is required"
if not email:
errors["email"] = "Email is required"
if not password:
errors["password"] = "Password is required"
if password != confirm_password:
errors["confirm_password"] = "Passwords do not match"
try:
storage_quota_gb = int(storage_quota_gb)
if storage_quota_gb < 1:
errors["storage_quota_gb"] = "Storage quota must be at least 1 GB"
except ValueError:
errors["storage_quota_gb"] = "Invalid storage quota value"
if errors:
return aiohttp_jinja2.render_template(
"pages/add_user.html",
self.request,
{
"request": self.request,
"user": self.request["user"],
"errors": errors,
"form_data": data,
"active_page": "users"
}
)
user_service = self.request.app["user_service"]
parent_email = self.request["user"]["email"]
try:
new_user = user_service.create_user(
full_name=full_name,
email=email,
password=password,
parent_email=parent_email
)
user_service.update_user_quota(email, float(storage_quota_gb))
return web.HTTPFound(
self.request.app.router["users"].url_for().with_query(
success=f"User {email} added successfully"
)
)
except ValueError as e:
errors["email"] = str(e)
return aiohttp_jinja2.render_template(
"pages/add_user.html",
self.request,
{
"request": self.request,
"user": self.request["user"],
"errors": errors,
"form_data": data,
"active_page": "users"
}
)
async def edit_user_page(self):
email = self.request.match_info.get("email")
user_service = self.request.app["user_service"]
user_data = user_service.get_user_by_email(email)
if not user_data:
return web.HTTPNotFound(text="User not found")
success_message = self.request.query.get("success")
return aiohttp_jinja2.render_template(
"pages/edit_user.html",
self.request,
{
"request": self.request,
"user": self.request["user"],
"user_data": user_data,
"errors": {},
"success_message": success_message,
"active_page": "users"
}
)
async def edit_user_submit(self):
email = self.request.match_info.get("email")
data = await self.request.post()
storage_quota_gb = data.get("storage_quota_gb", "")
errors = {}
try:
storage_quota_gb = float(storage_quota_gb)
if storage_quota_gb < 1:
errors["storage_quota_gb"] = "Storage quota must be at least 1 GB"
except ValueError:
errors["storage_quota_gb"] = "Invalid storage quota value"
user_service = self.request.app["user_service"]
user_data = user_service.get_user_by_email(email)
if not user_data:
return web.HTTPNotFound(text="User not found")
if errors:
return aiohttp_jinja2.render_template(
"pages/edit_user.html",
self.request,
{
"request": self.request,
"user": self.request["user"],
"user_data": user_data,
"errors": errors,
"active_page": "users"
}
)
user_service.update_user_quota(email, storage_quota_gb)
return web.HTTPFound(
self.request.app.router["edit_user"].url_for(email=email).with_query(
success="User quota updated successfully"
)
)
async def user_details_page(self):
email = self.request.match_info.get("email")
user_service = self.request.app["user_service"]
user_data = user_service.get_user_by_email(email)
if not user_data:
return web.HTTPNotFound(text="User not found")
return aiohttp_jinja2.render_template(
"pages/user_details.html",
self.request,
{
"request": self.request,
"user": self.request["user"],
"user_data": user_data,
"active_page": "users"
}
)
async def delete_user_submit(self):
email = self.request.match_info.get("email")
user_service = self.request.app["user_service"]
user_data = user_service.get_user_by_email(email)
if not user_data:
return web.HTTPNotFound(text="User not found")
user_service.delete_user(email)
return web.HTTPFound(
self.request.app.router["users"].url_for().with_query(
success=f"User {email} deleted successfully"
)
)

View File

@ -190,10 +190,8 @@ async def test_file_browser_get_authorized(client):
resp = await client.get("/files")
assert resp.status == 200
text = await resp.text()
assert "File Browser" in text
# Check for some expected files from the project directory
assert "example.jpg" in text
assert "rexample7.jpg" in text
assert "My Files" in text
assert "No files found in this directory." in text