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 .routes import setup_routes
from .services.user_service import UserService from .services.user_service import UserService
from .services.config_service import ConfigService from .services.config_service import ConfigService
from .services.file_service import FileService # Import FileService
from .middlewares import user_middleware, error_middleware 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 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" data_path = base_path.parent / "data"
app["user_service"] = UserService(data_path / "users.json") app["user_service"] = UserService(data_path / "users.json")
app["config_service"] = ConfigService(data_path / "config.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 # Setup aiojobs scheduler
app["scheduler"] = aiojobs.Scheduler() app["scheduler"] = aiojobs.Scheduler()

View File

@ -1,5 +1,5 @@
from .views.auth import LoginView, RegistrationView, LogoutView, ForgotPasswordView, ResetPasswordView 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 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("/recent", SiteView, name="recent")
app.router.add_view("/favorites", SiteView, name="favorites") app.router.add_view("/favorites", SiteView, name="favorites")
app.router.add_view("/trash", SiteView, name="trash") 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_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 # Admin API routes for user and team management
app.router.add_get("/api/users", get_users, name="api_get_users") app.router.add_get("/api/users", get_users, name="api_get_users")

View File

@ -303,6 +303,63 @@ main {
font-weight: 600; 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 { .error {
color: #D32F2F; /* Red for errors */ color: #D32F2F; /* Red for errors */
background-color: #FFEBEE; /* Light red background */ background-color: #FFEBEE; /* Light red background */

View File

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

View File

@ -1,50 +1,238 @@
/* retoors/static/css/components/file_browser.css */ .modal {
display: none;
.file-browser-section { position: fixed;
padding: 2rem; z-index: 1000;
max-width: 800px; left: 0;
margin: 0 auto; top: 0;
background-color: var(--color-background-light); width: 100%;
border-radius: var(--border-radius); height: 100%;
box-shadow: var(--shadow-elevation-low); overflow: auto;
background-color: rgba(0, 0, 0, 0.5);
} }
.file-browser-section h1 { .modal-content {
color: var(--color-primary); background-color: var(--card-background);
text-align: center; margin: 10% auto;
margin-bottom: 1.5rem; padding: 30px;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
} }
.file-list ul { .modal-content h3 {
list-style: none; margin-top: 0;
padding: 0; margin-bottom: 20px;
color: var(--text-color);
} }
.file-list li { .close {
background-color: var(--color-surface); color: var(--light-text-color);
margin-bottom: 0.5rem; float: right;
padding: 0.8rem 1rem; font-size: 28px;
border-radius: var(--border-radius-small); 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; display: flex;
align-items: center;
justify-content: space-between; justify-content: space-between;
border: 1px solid var(--color-border); font-size: 0.85rem;
color: var(--light-text-color);
} }
.file-list li a { .file-list-table table {
color: var(--color-text); 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; text-decoration: none;
flex-grow: 1;
font-weight: var(--font-weight-medium);
} }
.file-list li a:hover { .file-list-table a:hover {
color: var(--color-primary-dark);
text-decoration: underline; text-decoration: underline;
} }
.file-list p { @media (max-width: 768px) {
text-align: center; .modal-content {
color: var(--color-text-secondary); width: 95%;
font-style: italic; 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) */ /* Styles for the Quota Management / Admin Dashboard (order.html) */
.order-management-layout { .order-management-layout {
padding: 40px 20px; padding: 0;
max-width: 1200px;
margin: 0 auto;
} }
.quota-overview-card { .quota-overview-card {
background-color: var(--card-background); background-color: var(--card-background);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 6px 20px var(--shadow-color); box-shadow: 0 4px 12px var(--shadow-color);
padding: 30px; padding: 20px;
margin-bottom: 40px; margin-bottom: 30px;
text-align: center;
} }
.quota-overview-card h2 { .quota-overview-card h2 {
font-size: 2.5rem; font-size: 1.8rem;
color: var(--text-color); color: var(--text-color);
margin-bottom: 10px; margin-bottom: 10px;
} }
.quota-overview-card .subtitle { .quota-overview-card .subtitle {
font-size: 1.1rem; font-size: 0.95rem;
color: var(--light-text-color); color: var(--light-text-color);
margin-bottom: 30px; margin-bottom: 20px;
} }
.overview-grid { .overview-grid {
@ -35,17 +32,16 @@
} }
.total-storage-chart { .total-storage-chart {
text-align: center;
padding: 20px; padding: 20px;
border-radius: 8px; 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); box-shadow: 0 2px 8px rgba(0,0,0,0.05);
} }
.total-storage-chart h3 { .total-storage-chart h3 {
font-size: 1.3rem; font-size: 1.1rem;
color: var(--text-color); color: var(--text-color);
margin-bottom: 20px; margin-bottom: 15px;
} }
.donut-chart-container { .donut-chart-container {
@ -96,15 +92,13 @@
background-color: var(--background-color); background-color: var(--background-color);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05); box-shadow: 0 2px 8px rgba(0,0,0,0.05);
padding: 30px; padding: 20px;
text-align: left;
} }
.order-form-card h3 { .order-form-card h3 {
font-size: 1.3rem; font-size: 1.1rem;
color: var(--text-color); color: var(--text-color);
margin-bottom: 20px; margin-bottom: 15px;
text-align: center;
} }
.order-form { .order-form {
@ -119,24 +113,21 @@
} }
.order-form .price-display { .order-form .price-display {
font-size: 1.4rem; font-size: 1rem;
font-weight: 700; font-weight: 600;
color: var(--accent-color); color: var(--accent-color);
text-align: center;
margin-top: 10px; margin-top: 10px;
} }
.order-form .btn-primary { .order-form .btn-primary {
width: 100%; width: 100%;
padding: 12px;
font-size: 1.1rem;
} }
.user-quotas-section { .user-quotas-section {
background-color: var(--card-background); background-color: var(--card-background);
border-radius: 8px; border-radius: 8px;
box-shadow: 0 6px 20px var(--shadow-color); box-shadow: 0 4px 12px var(--shadow-color);
padding: 30px; padding: 20px;
} }
.user-quotas-header { .user-quotas-header {
@ -149,16 +140,11 @@
} }
.user-quotas-header h2 { .user-quotas-header h2 {
font-size: 2rem; font-size: 1.8rem;
color: var(--text-color); color: var(--text-color);
margin: 0; margin: 0;
} }
.user-quotas-header .btn-primary {
padding: 0.7rem 1.5rem;
font-size: 0.95rem;
}
.user-quota-list { .user-quota-list {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
@ -176,7 +162,7 @@
} }
.user-quota-item .user-info h4 { .user-quota-item .user-info h4 {
font-size: 1.2rem; font-size: 1.1rem;
color: var(--text-color); color: var(--text-color);
margin-bottom: 5px; margin-bottom: 5px;
} }
@ -199,16 +185,14 @@
.user-quota-item .quota-actions { .user-quota-item .quota-actions {
display: flex; display: flex;
flex-wrap: wrap; /* Allow buttons to wrap */ flex-wrap: wrap;
gap: 10px; gap: 10px;
margin-top: 15px; margin-top: 15px;
} }
.user-quota-item .quota-actions .btn-outline { .user-quota-item .quota-actions .btn-outline {
padding: 0.5rem 1rem; flex-grow: 1;
font-size: 0.85rem; min-width: 100px;
flex-grow: 1; /* Allow buttons to grow and fill space */
min-width: 100px; /* Ensure buttons don't get too small */
} }
/* Small storage gauge for user items */ /* Small storage gauge for user items */
@ -259,8 +243,7 @@
color: var(--text-color); color: var(--text-color);
margin-top: 0; margin-top: 0;
margin-bottom: 20px; margin-bottom: 20px;
font-size: 1.8rem; font-size: 1.2rem;
text-align: center;
} }
.modal-content .form-group { .modal-content .form-group {
@ -289,15 +272,12 @@
.modal-content .btn-primary { .modal-content .btn-primary {
width: 100%; width: 100%;
padding: 12px;
font-size: 1.1rem;
margin-top: 20px; margin-top: 20px;
} }
.modal-content .error { .modal-content .error {
color: var(--error-color); color: var(--error-color);
margin-top: 10px; margin-top: 10px;
text-align: center;
} }
.close-button { .close-button {
@ -344,17 +324,14 @@
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.order-management-layout {
padding: 20px 15px;
}
.quota-overview-card h2 { .quota-overview-card h2 {
font-size: 2rem; font-size: 1.5rem;
} }
.quota-overview-card .subtitle { .quota-overview-card .subtitle {
font-size: 1rem; font-size: 0.9rem;
} }
.user-quotas-header h2 { .user-quotas-header h2 {
font-size: 1.8rem; font-size: 1.5rem;
} }
.user-quota-list { .user-quota-list {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -367,10 +344,10 @@
@media (max-width: 480px) { @media (max-width: 480px) {
.quota-overview-card { .quota-overview-card {
padding: 20px; padding: 15px;
} }
.quota-overview-card h2 { .quota-overview-card h2 {
font-size: 1.8rem; font-size: 1.2rem;
} }
.donut-chart-container { .donut-chart-container {
width: 150px; width: 150px;
@ -381,19 +358,19 @@
height: 110px; height: 110px;
} }
.donut-chart-text { .donut-chart-text {
font-size: 1.2rem; font-size: 0.95rem;
} }
.order-form-card { .order-form-card {
padding: 20px; padding: 15px;
} }
.user-quotas-section { .user-quotas-section {
padding: 20px; padding: 15px;
} }
.user-quotas-header h2 { .user-quotas-header h2 {
font-size: 1.5rem; font-size: 1.1rem;
} }
.user-quota-item .quota-actions .btn-outline { .user-quota-item .quota-actions .btn-outline {
flex-grow: unset; /* Reset flex-grow for smaller screens if needed */ flex-grow: unset;
width: 100%; /* Make buttons full width */ width: 100%;
} }
} }

View File

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

View File

@ -11,7 +11,7 @@
<aside class="dashboard-sidebar"> <aside class="dashboard-sidebar">
<div class="sidebar-menu"> <div class="sidebar-menu">
<ul> <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="/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="/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> <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 title %}Favorites - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }} {% block page_title %}Favorites{% endblock %}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %} {% block dashboard_content %}
{% block content %} <div class="content-section">
<main> <p>Your favorite files and folders will appear here.</p>
<section class="content-section"> <p>This feature is coming soon.</p>
<h1>Favorites</h1> </div>
<p>Your favorite files and folders will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
{% endblock %} {% endblock %}

View File

@ -1,24 +1,295 @@
{% extends "layouts/base.html" %} {% extends "layouts/dashboard.html" %}
{% block title %}File Browser{% endblock %}
{% block head %} {% block title %}My Files - Retoor's Cloud Solutions{% endblock %}
{{ super() }}
{% block dashboard_head %}
<link rel="stylesheet" href="/static/css/components/file_browser.css"> <link rel="stylesheet" href="/static/css/components/file_browser.css">
{% endblock %} {% endblock %}
{% block content %}
<main> {% block page_title %}My Files{% endblock %}
<section class="file-browser-section">
<h1>File Browser</h1> {% block dashboard_actions %}
<div class="file-list"> <button class="btn-primary" onclick="showNewFolderModal()">+ New</button>
{% if files %} <button class="btn-outline" onclick="showUploadModal()">Upload</button>
<ul> <button class="btn-outline" onclick="downloadSelected()" id="download-btn" disabled>Download</button>
{% for file in files %} <button class="btn-outline" onclick="shareSelected()" id="share-btn" disabled>Share</button>
<li><a href="/files/{{ file }}" target="_blank">{{ file }}</a></li> <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 %} {% endfor %}
</ul> {% else %}
{% else %} <tr>
<p>No files found in the project directory.</p> <td colspan="6" style="text-align: center; padding: 40px;">
{% endif %} <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> </div>
</section> </form>
</main> </div>
{% endblock %} </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 title %}Quota Management - Retoor's Cloud Solutions{% endblock %}
{% block head %} {% block dashboard_head %}
<link rel="stylesheet" href="/static/css/components/order.css"> <link rel="stylesheet" href="/static/css/components/order.css">
{% endblock %} {% endblock %}
{% block content %} {% block page_title %}Manage Quota{% endblock %}
<main class="order-management-layout">
{% block dashboard_content %}
<div class="order-management-layout">
<section class="quota-overview-card"> <section class="quota-overview-card">
<h2>Optimize Your Team's Storage</h2> <h2>Optimize Your Team's Storage</h2>
<p class="subtitle">Flexible cloud monitoring for teams with ease.</p> <p class="subtitle">Flexible cloud monitoring for teams with ease.</p>
@ -46,75 +48,7 @@
</div> </div>
</section> </section>
<section class="user-quotas-section"> </div>
<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>
{# Add New User Modal #} <script src="/static/js/components/order_form.js"></script>
<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>
{% endblock %} {% 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 title %}Recent Files - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }} {% block page_title %}Recent Files{% endblock %}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %} {% block dashboard_content %}
{% block content %} <div class="content-section">
<main> <p>Your recently accessed files will appear here.</p>
<section class="content-section"> <p>This feature is coming soon.</p>
<h1>Recent Files</h1> </div>
<p>Your recently accessed files will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
{% endblock %} {% 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 title %}Shared with me - Retoor's Cloud Solutions{% endblock %}
{% block head %}
{{ super() }} {% block page_title %}Shared with me{% endblock %}
<link rel="stylesheet" href="/static/css/components/content_pages.css">
{% endblock %} {% block dashboard_content %}
{% block content %} <div class="content-section">
<main> <p>Files and folders that have been shared with you will appear here.</p>
<section class="content-section"> <p>This feature is coming soon.</p>
<h1>Shared with me</h1> </div>
<p>Files and folders that have been shared with you will appear here.</p>
<p>This feature is coming soon!</p>
</section>
</main>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

@ -2,11 +2,13 @@ from aiohttp import web
import aiohttp_jinja2 import aiohttp_jinja2
import os import os
from pathlib import Path from pathlib import Path
from aiohttp.web_response import json_response
from ..helpers.auth import login_required from ..helpers.auth import login_required
from .auth import CustomPydanticView 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): class SiteView(web.View):
@ -35,17 +37,15 @@ class SiteView(web.View):
return await self.favorites() return await self.favorites()
elif self.request.path == "/trash": elif self.request.path == "/trash":
return await self.trash() return await self.trash()
elif self.request.path == "/users":
return await self.users()
return aiohttp_jinja2.render_template( return aiohttp_jinja2.render_template(
"pages/index.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")} "pages/index.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
) )
@login_required @login_required
async def dashboard(self): async def dashboard(self):
return aiohttp_jinja2.render_template( return web.HTTPFound(self.request.app.router["file_browser"].url_for())
"pages/dashboard.html",
self.request,
{"user": self.request["user"], "request": self.request, "errors": {}},
)
async def solutions(self): async def solutions(self):
return aiohttp_jinja2.render_template( 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")} "pages/security.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
) )
@login_required
async def support(self): async def support(self):
return aiohttp_jinja2.render_template( 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): 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")} "pages/privacy.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
) )
@login_required
async def shared(self): async def shared(self):
return aiohttp_jinja2.render_template( 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): async def recent(self):
return aiohttp_jinja2.render_template( 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): async def favorites(self):
return aiohttp_jinja2.render_template( 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): async def trash(self):
return aiohttp_jinja2.render_template( 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): class FileBrowserView(web.View):
@login_required @login_required
async def get(self): async def get(self):
files = [] if self.request.match_info.get("file_path") is not None:
if PROJECT_DIR.is_dir(): return await self.get_download_file()
for item in os.listdir(PROJECT_DIR):
item_path = PROJECT_DIR / item user_email = self.request["user"]["email"]
if item_path.is_file(): file_service = self.request.app["file_service"]
files.append(item)
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( return aiohttp_jinja2.render_template(
"pages/file_browser.html", "pages/file_browser.html",
self.request, 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): class OrderView(CustomPydanticView):
template_name = "pages/order.html" template_name = "pages/order.html"
@ -130,22 +290,236 @@ class OrderView(CustomPydanticView):
self.template_name, self.request, { self.template_name, self.request, {
"request": self.request, "request": self.request,
"errors": {}, "errors": {},
"user": self.request.get("user"), "user": self.request["user"],
"price_per_gb": price_per_gb "price_per_gb": price_per_gb,
"active_page": "order"
} }
) )
@login_required @login_required
async def post(self): 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"] config_service = self.request.app["config_service"]
price_per_gb = config_service.get_price_per_gb() price_per_gb = config_service.get_price_per_gb()
return aiohttp_jinja2.render_template( return aiohttp_jinja2.render_template(
self.template_name, self.request, { self.template_name, self.request, {
"request": self.request, "request": self.request,
"errors": {}, "errors": {},
"user": self.request.get("user"), "user": self.request["user"],
"price_per_gb": price_per_gb "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") resp = await client.get("/files")
assert resp.status == 200 assert resp.status == 200
text = await resp.text() text = await resp.text()
assert "File Browser" in text assert "My Files" in text
# Check for some expected files from the project directory assert "No files found in this directory." in text
assert "example.jpg" in text
assert "rexample7.jpg" in text