Update.
This commit is contained in:
parent
2ad7401226
commit
a6a19b8438
@ -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()
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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 */
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
@ -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')">×</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')">×</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')">×</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')">×</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 %}
|
||||
@ -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">×</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">×</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">×</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 %}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -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 %}
|
||||
@ -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"
|
||||
)
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user