Compare commits
2 Commits
925f91a17c
...
cd259b0b81
| Author | SHA1 | Date | |
|---|---|---|---|
| cd259b0b81 | |||
| d8a419f528 |
@ -1,4 +1,6 @@
|
|||||||
import aiosmtplib
|
import aiosmtplib
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
from email.message import EmailMessage
|
from email.message import EmailMessage
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import logging
|
import logging
|
||||||
@ -24,11 +26,12 @@ async def send_email(app: web.Application, recipient_email: str, subject: str, b
|
|||||||
logger.error("SMTP host or sender email not configured. Cannot send email.")
|
logger.error("SMTP host or sender email not configured. Cannot send email.")
|
||||||
return
|
return
|
||||||
|
|
||||||
msg = EmailMessage()
|
msg = MIMEMultipart('alternative')
|
||||||
msg["From"] = smtp_sender_email
|
msg["From"] = smtp_sender_email
|
||||||
msg["To"] = recipient_email
|
msg["To"] = recipient_email
|
||||||
msg["Subject"] = subject
|
msg["Subject"] = subject
|
||||||
msg.set_content(body)
|
html_part = MIMEText(body, 'html')
|
||||||
|
msg.attach(html_part)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await aiosmtplib.send(
|
await aiosmtplib.send(
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
|
||||||
|
import argparse
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
import aiohttp_jinja2
|
import aiohttp_jinja2
|
||||||
import jinja2
|
import jinja2
|
||||||
@ -20,7 +22,7 @@ async def setup_services(app: web.Application):
|
|||||||
data_path = base_path.parent / "data"
|
data_path = base_path.parent / "data"
|
||||||
app["user_service"] = UserService(use_isolated_storage=True)
|
app["user_service"] = UserService(use_isolated_storage=True)
|
||||||
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")
|
app["file_service"] = FileService(data_path / "user_files", app["user_service"])
|
||||||
|
|
||||||
# Setup aiojobs scheduler
|
# Setup aiojobs scheduler
|
||||||
app["scheduler"] = aiojobs.Scheduler()
|
app["scheduler"] = aiojobs.Scheduler()
|
||||||
@ -67,9 +69,15 @@ def create_app():
|
|||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('--port', type=int, default=8080)
|
||||||
|
parser.add_argument('--hostname', default='0.0.0.0')
|
||||||
|
args = parser.parse_args()
|
||||||
app = create_app()
|
app = create_app()
|
||||||
web.run_app(app)
|
web.run_app(app, host=args.hostname, port=args.port)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -3,7 +3,8 @@ from .views.site import SiteView, OrderView, FileBrowserView, UserManagementView
|
|||||||
from .views.upload import UploadView
|
from .views.upload import UploadView
|
||||||
from .views.migrate import MigrateView
|
from .views.migrate import MigrateView
|
||||||
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
|
||||||
|
from .views.editor import FileEditorView, FileContentView
|
||||||
|
from .views.viewer import ViewerView
|
||||||
|
|
||||||
def setup_routes(app):
|
def setup_routes(app):
|
||||||
app.router.add_view("/login", LoginView, name="login")
|
app.router.add_view("/login", LoginView, name="login")
|
||||||
@ -21,6 +22,12 @@ def setup_routes(app):
|
|||||||
app.router.add_view("/order", OrderView, name="order")
|
app.router.add_view("/order", OrderView, name="order")
|
||||||
app.router.add_view("/terms", SiteView, name="terms")
|
app.router.add_view("/terms", SiteView, name="terms")
|
||||||
app.router.add_view("/privacy", SiteView, name="privacy")
|
app.router.add_view("/privacy", SiteView, name="privacy")
|
||||||
|
app.router.add_view("/cookies", SiteView, name="cookies")
|
||||||
|
app.router.add_view("/impressum", SiteView, name="impressum")
|
||||||
|
app.router.add_view("/user_rights", SiteView, name="user_rights")
|
||||||
|
app.router.add_view("/aup", SiteView, name="aup")
|
||||||
|
app.router.add_view("/sla", SiteView, name="sla")
|
||||||
|
app.router.add_view("/compliance", SiteView, name="compliance")
|
||||||
app.router.add_view("/shared", SiteView, name="shared")
|
app.router.add_view("/shared", SiteView, name="shared")
|
||||||
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")
|
||||||
@ -40,6 +47,10 @@ def setup_routes(app):
|
|||||||
app.router.add_post("/files/share_multiple", FileBrowserView, name="share_multiple_items")
|
app.router.add_post("/files/share_multiple", FileBrowserView, name="share_multiple_items")
|
||||||
app.router.add_get("/shared_file/{share_id}", FileBrowserView.shared_file_handler, name="shared_file")
|
app.router.add_get("/shared_file/{share_id}", FileBrowserView.shared_file_handler, name="shared_file")
|
||||||
app.router.add_get("/shared_file/{share_id}/download", FileBrowserView.download_shared_file_handler, name="download_shared_file")
|
app.router.add_get("/shared_file/{share_id}/download", FileBrowserView.download_shared_file_handler, name="download_shared_file")
|
||||||
|
app.router.add_view("/editor", FileEditorView, name="file_editor")
|
||||||
|
app.router.add_view("/viewer", ViewerView, name="file_viewer")
|
||||||
|
app.router.add_get("/api/file/content", FileContentView, name="get_file_content")
|
||||||
|
app.router.add_post("/api/file/save", FileEditorView, name="save_file_content")
|
||||||
|
|
||||||
# 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")
|
||||||
|
|||||||
@ -137,6 +137,63 @@ class FileService:
|
|||||||
logger.info(f"download_file: Successfully read file: {file_path}")
|
logger.info(f"download_file: Successfully read file: {file_path}")
|
||||||
return content, Path(file_path).name
|
return content, Path(file_path).name
|
||||||
|
|
||||||
|
async def read_file_content_binary(self, user_email: str, file_path: str) -> str | None:
|
||||||
|
"""Reads file content as text for editing."""
|
||||||
|
metadata = await self._load_metadata(user_email)
|
||||||
|
if file_path not in metadata or metadata[file_path]["type"] != "file":
|
||||||
|
logger.warning(f"read_file_content: File not found in metadata: {file_path}")
|
||||||
|
return None
|
||||||
|
item_meta = metadata[file_path]
|
||||||
|
blob_loc = item_meta["blob_location"]
|
||||||
|
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
|
||||||
|
if not blob_path.exists():
|
||||||
|
logger.warning(f"read_file_content: Blob not found: {blob_path}")
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(blob_path, 'rb') as f:
|
||||||
|
content = await f.read()
|
||||||
|
logger.info(f"read_file_content: Successfully read file: {file_path}")
|
||||||
|
return content
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
logger.warning(f"read_file_content: File is not a text file: {file_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def read_file_content(self, user_email: str, file_path: str) -> str | None:
|
||||||
|
"""Reads file content as text for editing."""
|
||||||
|
metadata = await self._load_metadata(user_email)
|
||||||
|
if file_path not in metadata or metadata[file_path]["type"] != "file":
|
||||||
|
logger.warning(f"read_file_content: File not found in metadata: {file_path}")
|
||||||
|
return None
|
||||||
|
item_meta = metadata[file_path]
|
||||||
|
blob_loc = item_meta["blob_location"]
|
||||||
|
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
|
||||||
|
if not blob_path.exists():
|
||||||
|
logger.warning(f"read_file_content: Blob not found: {blob_path}")
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
async with aiofiles.open(blob_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = await f.read()
|
||||||
|
logger.info(f"read_file_content: Successfully read file: {file_path}")
|
||||||
|
return content
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
logger.warning(f"read_file_content: File is not a text file: {file_path}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def save_file_content(self, user_email: str, file_path: str, content: str) -> bool:
|
||||||
|
"""Saves file content from editor."""
|
||||||
|
try:
|
||||||
|
content_bytes = content.encode('utf-8')
|
||||||
|
success = await self.upload_file(user_email, file_path, content_bytes)
|
||||||
|
if success:
|
||||||
|
logger.info(f"save_file_content: Successfully saved file: {file_path}")
|
||||||
|
return success
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"save_file_content: Error saving file {file_path}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
async def delete_item(self, user_email: str, item_path: str) -> bool:
|
async def delete_item(self, user_email: str, item_path: str) -> bool:
|
||||||
"""Deletes a file or folder for the user."""
|
"""Deletes a file or folder for the user."""
|
||||||
metadata = await self._load_metadata(user_email)
|
metadata = await self._load_metadata(user_email)
|
||||||
|
|||||||
29
retoors/services/lock_manager.py
Normal file
29
retoors/services/lock_manager.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import asyncio
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class LockManager:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._locks: Dict[str, asyncio.Lock] = {}
|
||||||
|
self._master_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def get_lock(self, identifier: str) -> asyncio.Lock:
|
||||||
|
async with self._master_lock:
|
||||||
|
if identifier not in self._locks:
|
||||||
|
self._locks[identifier] = asyncio.Lock()
|
||||||
|
return self._locks[identifier]
|
||||||
|
|
||||||
|
async def cleanup_unused_locks(self):
|
||||||
|
async with self._master_lock:
|
||||||
|
to_remove = [key for key, lock in self._locks.items() if not lock.locked()]
|
||||||
|
for key in to_remove:
|
||||||
|
del self._locks[key]
|
||||||
|
|
||||||
|
|
||||||
|
_global_lock_manager = LockManager()
|
||||||
|
|
||||||
|
|
||||||
|
def get_lock_manager() -> LockManager:
|
||||||
|
return _global_lock_manager
|
||||||
@ -4,6 +4,7 @@ import hashlib
|
|||||||
import aiofiles
|
import aiofiles
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
from .lock_manager import get_lock_manager
|
||||||
|
|
||||||
|
|
||||||
class StorageService:
|
class StorageService:
|
||||||
@ -11,6 +12,7 @@ class StorageService:
|
|||||||
def __init__(self, base_path: str = "data/user"):
|
def __init__(self, base_path: str = "data/user"):
|
||||||
self.base_path = Path(base_path)
|
self.base_path = Path(base_path)
|
||||||
self.base_path.mkdir(parents=True, exist_ok=True)
|
self.base_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.lock_manager = get_lock_manager()
|
||||||
|
|
||||||
def _hash(self, value: str) -> str:
|
def _hash(self, value: str) -> str:
|
||||||
return hashlib.sha256(value.encode()).hexdigest()
|
return hashlib.sha256(value.encode()).hexdigest()
|
||||||
@ -43,6 +45,8 @@ class StorageService:
|
|||||||
|
|
||||||
file_path.parent.mkdir(parents=True, exist_ok=True)
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
lock = await self.lock_manager.get_lock(str(file_path))
|
||||||
|
async with lock:
|
||||||
async with aiofiles.open(file_path, 'w') as f:
|
async with aiofiles.open(file_path, 'w') as f:
|
||||||
await f.write(json.dumps(data, indent=2))
|
await f.write(json.dumps(data, indent=2))
|
||||||
|
|
||||||
@ -58,6 +62,8 @@ class StorageService:
|
|||||||
if not file_path.exists():
|
if not file_path.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
lock = await self.lock_manager.get_lock(str(file_path))
|
||||||
|
async with lock:
|
||||||
async with aiofiles.open(file_path, 'r') as f:
|
async with aiofiles.open(file_path, 'r') as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
if not content:
|
if not content:
|
||||||
@ -74,6 +80,9 @@ class StorageService:
|
|||||||
if not self._validate_path(file_path, user_base):
|
if not self._validate_path(file_path, user_base):
|
||||||
raise ValueError("Invalid path: directory traversal detected")
|
raise ValueError("Invalid path: directory traversal detected")
|
||||||
|
|
||||||
|
if file_path.exists():
|
||||||
|
lock = await self.lock_manager.get_lock(str(file_path))
|
||||||
|
async with lock:
|
||||||
if file_path.exists():
|
if file_path.exists():
|
||||||
file_path.unlink()
|
file_path.unlink()
|
||||||
return True
|
return True
|
||||||
@ -98,6 +107,8 @@ class StorageService:
|
|||||||
results = []
|
results = []
|
||||||
for json_file in user_base.rglob("*.json"):
|
for json_file in user_base.rglob("*.json"):
|
||||||
if self._validate_path(json_file, user_base):
|
if self._validate_path(json_file, user_base):
|
||||||
|
lock = await self.lock_manager.get_lock(str(json_file))
|
||||||
|
async with lock:
|
||||||
async with aiofiles.open(json_file, 'r') as f:
|
async with aiofiles.open(json_file, 'r') as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
results.append(json.loads(content))
|
results.append(json.loads(content))
|
||||||
@ -122,6 +133,7 @@ class UserStorageManager:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.storage = StorageService()
|
self.storage = StorageService()
|
||||||
|
self.lock_manager = get_lock_manager()
|
||||||
|
|
||||||
async def save_user(self, user_email: str, user_data: Dict[str, Any]) -> bool:
|
async def save_user(self, user_email: str, user_data: Dict[str, Any]) -> bool:
|
||||||
return await self.storage.save(user_email, user_email, user_data)
|
return await self.storage.save(user_email, user_email, user_data)
|
||||||
@ -146,6 +158,8 @@ class UserStorageManager:
|
|||||||
if user_dir.is_dir():
|
if user_dir.is_dir():
|
||||||
user_files = list(user_dir.rglob("*.json"))
|
user_files = list(user_dir.rglob("*.json"))
|
||||||
for user_file in user_files:
|
for user_file in user_files:
|
||||||
|
lock = await self.lock_manager.get_lock(str(user_file))
|
||||||
|
async with lock:
|
||||||
async with aiofiles.open(user_file, 'r') as f:
|
async with aiofiles.open(user_file, 'r') as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
user_data = json.loads(content)
|
user_data = json.loads(content)
|
||||||
@ -165,6 +179,8 @@ class UserStorageManager:
|
|||||||
if user_dir.is_dir():
|
if user_dir.is_dir():
|
||||||
user_files = list(user_dir.rglob("*.json"))
|
user_files = list(user_dir.rglob("*.json"))
|
||||||
for user_file in user_files:
|
for user_file in user_files:
|
||||||
|
lock = await self.lock_manager.get_lock(str(user_file))
|
||||||
|
async with lock:
|
||||||
async with aiofiles.open(user_file, 'r') as f:
|
async with aiofiles.open(user_file, 'r') as f:
|
||||||
content = await f.read()
|
content = await f.read()
|
||||||
all_users.append(json.loads(content))
|
all_users.append(json.loads(content))
|
||||||
|
|||||||
@ -5,11 +5,13 @@ import bcrypt
|
|||||||
import secrets
|
import secrets
|
||||||
import datetime
|
import datetime
|
||||||
from .storage_service import UserStorageManager
|
from .storage_service import UserStorageManager
|
||||||
|
from .lock_manager import get_lock_manager
|
||||||
|
|
||||||
|
|
||||||
class UserService:
|
class UserService:
|
||||||
def __init__(self, users_path: Path = None, use_isolated_storage: bool = True):
|
def __init__(self, users_path: Path = None, use_isolated_storage: bool = True):
|
||||||
self.use_isolated_storage = use_isolated_storage
|
self.use_isolated_storage = use_isolated_storage
|
||||||
|
self.lock_manager = get_lock_manager()
|
||||||
|
|
||||||
if use_isolated_storage:
|
if use_isolated_storage:
|
||||||
self._storage_manager = UserStorageManager()
|
self._storage_manager = UserStorageManager()
|
||||||
@ -27,9 +29,11 @@ class UserService:
|
|||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _save_users(self):
|
async def _save_users(self):
|
||||||
if self.use_isolated_storage:
|
if self.use_isolated_storage:
|
||||||
return
|
return
|
||||||
|
lock = await self.lock_manager.get_lock(str(self._users_path))
|
||||||
|
async with lock:
|
||||||
with open(self._users_path, "w") as f:
|
with open(self._users_path, "w") as f:
|
||||||
json.dump(self._users, f, indent=4)
|
json.dump(self._users, f, indent=4)
|
||||||
|
|
||||||
@ -80,7 +84,7 @@ class UserService:
|
|||||||
await self._storage_manager.save_user(email, user)
|
await self._storage_manager.save_user(email, user)
|
||||||
else:
|
else:
|
||||||
self._users.append(user)
|
self._users.append(user)
|
||||||
self._save_users()
|
await self._save_users()
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@ -98,7 +102,7 @@ class UserService:
|
|||||||
if self.use_isolated_storage:
|
if self.use_isolated_storage:
|
||||||
await self._storage_manager.save_user(email, user)
|
await self._storage_manager.save_user(email, user)
|
||||||
else:
|
else:
|
||||||
self._save_users()
|
await self._save_users()
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
@ -109,7 +113,7 @@ class UserService:
|
|||||||
initial_len = len(self._users)
|
initial_len = len(self._users)
|
||||||
self._users = [user for user in self._users if user["email"] != email]
|
self._users = [user for user in self._users if user["email"] != email]
|
||||||
if len(self._users) < initial_len:
|
if len(self._users) < initial_len:
|
||||||
self._save_users()
|
await self._save_users()
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -126,7 +130,7 @@ class UserService:
|
|||||||
self._users = [user for user in self._users if user.get("parent_email") != parent_email]
|
self._users = [user for user in self._users if user.get("parent_email") != parent_email]
|
||||||
deleted_count = initial_len - len(self._users)
|
deleted_count = initial_len - len(self._users)
|
||||||
if deleted_count > 0:
|
if deleted_count > 0:
|
||||||
self._save_users()
|
await self._save_users()
|
||||||
|
|
||||||
return deleted_count
|
return deleted_count
|
||||||
|
|
||||||
@ -160,7 +164,7 @@ class UserService:
|
|||||||
if self.use_isolated_storage:
|
if self.use_isolated_storage:
|
||||||
await self._storage_manager.save_user(email, user)
|
await self._storage_manager.save_user(email, user)
|
||||||
else:
|
else:
|
||||||
self._save_users()
|
await self._save_users()
|
||||||
|
|
||||||
return token
|
return token
|
||||||
|
|
||||||
@ -192,7 +196,7 @@ class UserService:
|
|||||||
if self.use_isolated_storage:
|
if self.use_isolated_storage:
|
||||||
await self._storage_manager.save_user(email, user)
|
await self._storage_manager.save_user(email, user)
|
||||||
else:
|
else:
|
||||||
self._save_users()
|
await self._save_users()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -206,4 +210,4 @@ class UserService:
|
|||||||
if self.use_isolated_storage:
|
if self.use_isolated_storage:
|
||||||
await self._storage_manager.save_user(email, user)
|
await self._storage_manager.save_user(email, user)
|
||||||
else:
|
else:
|
||||||
self._save_users()
|
await self._save_users()
|
||||||
|
|||||||
@ -1,64 +1,72 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary-color: #4A90E2; /* Blue from image */
|
--primary-color: #0066FF; /* Vibrant blue */
|
||||||
--accent-color: #50E3C2; /* Greenish-blue from image */
|
--accent-color: #00D4FF; /* Bright cyan accent */
|
||||||
--secondary-color: #B8C2CC; /* Light grey-blue */
|
--secondary-color: #F7FAFC; /* Very light blue-grey */
|
||||||
--background-color: #F8F8F8; /* Very light grey background */
|
--background-color: #FFFFFF; /* Pure white background */
|
||||||
--text-color: #333333; /* Darker text */
|
--text-color: #1A202C; /* Dark blue-grey */
|
||||||
--light-text-color: #666666; /* Lighter text for descriptions */
|
--light-text-color: #718096; /* Medium grey for descriptions */
|
||||||
--border-color: #E0E0E0; /* Light grey border */
|
--border-color: #E2E8F0; /* Light grey border */
|
||||||
--card-background: #FFFFFF; /* White for cards */
|
--card-background: #FFFFFF; /* White for cards */
|
||||||
--shadow-color: rgba(0, 0, 0, 0.08); /* Subtle shadow */
|
--shadow-color: rgba(0, 0, 0, 0.05); /* Very subtle shadow */
|
||||||
|
|
||||||
/* Button specific variables, using accent for primary CTAs */
|
/* Button specific variables */
|
||||||
--btn-primary-bg: var(--primary-color);
|
--btn-primary-bg: var(--primary-color);
|
||||||
--btn-primary-text: #FFFFFF;
|
--btn-primary-text: #FFFFFF;
|
||||||
--btn-primary-hover-bg: #3A7BD5; /* Slightly darker blue */
|
--btn-primary-hover-bg: #0052CC; /* Darker blue */
|
||||||
--btn-secondary-bg: #E0E0E0; /* Light grey for secondary buttons */
|
--btn-secondary-bg: #EDF2F7; /* Light grey */
|
||||||
--btn-secondary-text: var(--text-color);
|
--btn-secondary-text: var(--text-color);
|
||||||
--btn-secondary-hover-bg: #BDBDBD; /* Darker grey for secondary hover */
|
--btn-secondary-hover-bg: #E2E8F0; /* Darker grey */
|
||||||
--btn-outline-border: var(--primary-color);
|
--btn-outline-border: var(--primary-color);
|
||||||
--btn-outline-text: var(--primary-color);
|
--btn-outline-text: var(--primary-color);
|
||||||
--btn-outline-hover-bg: rgba(74, 144, 226, 0.1); /* Light blue hover */
|
--btn-outline-hover-bg: rgba(0, 102, 255, 0.05); /* Very light blue hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
html, body {
|
html, body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: 'Roboto', sans-serif; /* Using a more modern font */
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
line-height: 1.6; /* Improve readability */
|
line-height: 1.6;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* General typography */
|
/* General typography */
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2.2rem; /* Slightly larger heading */
|
font-size: 3.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1.5rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
line-height: 1.2;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 2.2rem;
|
font-size: 2.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin-bottom: 0.8rem;
|
margin-bottom: 1rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
line-height: 1.3;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1.8rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
margin-bottom: 0.6rem;
|
margin-bottom: 0.75rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
|
font-size: 1.125rem;
|
||||||
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card-like styling for sections */
|
/* Card-like styling for sections */
|
||||||
@ -147,96 +155,100 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Header and Navigation */
|
/* Header and Navigation */
|
||||||
header {
|
.site-header {
|
||||||
background-color: var(--card-background); /* White background for header */
|
background-color: #FFFFFF;
|
||||||
box-shadow: 0 2px 4px var(--shadow-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
padding: 15px 20px;
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
.site-nav {
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
min-height: 70px;
|
||||||
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .logo {
|
.brand {
|
||||||
|
text-decoration: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-decoration: none;
|
flex-shrink: 0;
|
||||||
color: var(--text-color);
|
}
|
||||||
|
|
||||||
|
.brand-text {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
color: var(--text-color);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .logo img {
|
.nav-menu {
|
||||||
height: 30px; /* Adjust logo size */
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
nav .nav-links {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
gap: 30px;
|
gap: 2rem;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .nav-links a {
|
.nav-menu li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu a {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: color 0.3s ease;
|
font-size: 1rem;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav .nav-links a:hover {
|
.nav-menu a:hover {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary-nav {
|
.nav-actions {
|
||||||
background-color: var(--primary-color);
|
display: flex;
|
||||||
color: white;
|
flex-direction: row;
|
||||||
padding: 10px 20px;
|
align-items: center;
|
||||||
border-radius: 5px;
|
gap: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: var(--text-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: background-color 0.3s ease;
|
font-size: 1rem;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary-nav:hover {
|
.nav-link:hover {
|
||||||
background-color: var(--btn-primary-hover-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-nav {
|
|
||||||
background-color: transparent;
|
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
border: 1px solid var(--primary-color);
|
|
||||||
padding: 10px 20px;
|
|
||||||
border-radius: 5px;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-outline-nav:hover {
|
|
||||||
background-color: rgba(74, 144, 226, 0.1);
|
|
||||||
color: var(--primary-color);
|
|
||||||
border-color: var(--primary-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main content area */
|
/* Main content area */
|
||||||
main {
|
main {
|
||||||
flex-grow: 1; /* Allow main content to take available space */
|
flex-grow: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 1200px; /* Match header max-width */
|
margin: 0;
|
||||||
margin: 0 auto; /* Center the main content */
|
padding: 0;
|
||||||
padding: 40px 20px; /* Add padding */
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: var(--background-color); /* Light background for main content */
|
background-color: var(--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Form specific styles */
|
/* Form specific styles */
|
||||||
@ -244,17 +256,17 @@ main {
|
|||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
margin: 50px auto;
|
margin: 50px auto;
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
border-radius: 12px; /* Slightly more rounded corners */
|
border-radius: 12px;
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); /* Softer, more prominent shadow */
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
padding: 3rem; /* Increased padding */
|
padding: 2.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-container h2 {
|
.form-container h2 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 35px; /* Increased margin */
|
margin-bottom: 2rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-size: 2.2rem; /* Slightly larger heading */
|
font-size: 2rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -309,16 +321,17 @@ main {
|
|||||||
.btn-small,
|
.btn-small,
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 0.6rem 1.2rem;
|
padding: 0.65rem 1.5rem;
|
||||||
font-size: 0.9rem;
|
font-size: 0.95rem;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
border: 1px solid transparent;
|
border: 2px solid transparent;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@ -330,6 +343,8 @@ main {
|
|||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background-color: var(--btn-primary-hover-bg);
|
background-color: var(--btn-primary-hover-bg);
|
||||||
border-color: var(--btn-primary-hover-bg);
|
border-color: var(--btn-primary-hover-bg);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
@ -342,6 +357,7 @@ main {
|
|||||||
background-color: var(--btn-outline-hover-bg);
|
background-color: var(--btn-outline-hover-bg);
|
||||||
color: var(--btn-outline-text);
|
color: var(--btn-outline-text);
|
||||||
border-color: var(--btn-outline-border);
|
border-color: var(--btn-outline-border);
|
||||||
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-small {
|
.btn-small {
|
||||||
@ -372,28 +388,81 @@ main {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments for base.css */
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 992px) {
|
||||||
|
.nav-menu {
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
nav {
|
.site-nav {
|
||||||
|
padding: 10px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-container {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
min-height: auto;
|
||||||
|
padding: 10px 0;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
justify-content: center;
|
||||||
|
order: 1;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 15px;
|
gap: 1rem;
|
||||||
}
|
order: 2;
|
||||||
nav .logo {
|
|
||||||
width: 100%;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
nav .nav-links {
|
|
||||||
width: 100%;
|
.nav-menu a {
|
||||||
justify-content: center;
|
font-size: 0.9rem;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
.btn-primary-nav {
|
|
||||||
|
.nav-actions {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
justify-content: center;
|
||||||
|
order: 3;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.brand-text {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu {
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-menu a {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-actions .btn-primary,
|
||||||
|
.nav-actions .btn-outline {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 280px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
main {
|
|
||||||
padding: 20px 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,3 +1,72 @@
|
|||||||
|
/* Content pages main wrapper */
|
||||||
|
main.content-page {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content sections for legal and information pages */
|
||||||
|
.content-section {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
margin-top: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section p {
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section ul {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 2rem 0;
|
||||||
|
background-color: var(--card-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section table th,
|
||||||
|
.content-section table td {
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section table th {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
/* General styles for hero sections on content pages */
|
/* General styles for hero sections on content pages */
|
||||||
.hero-intro {
|
.hero-intro {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
|||||||
@ -10,15 +10,25 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-sidebar {
|
.dashboard-sidebar {
|
||||||
flex: 0 0 250px; /* Fixed width sidebar */
|
flex: 0 0 250px;
|
||||||
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: 20px;
|
padding: 20px;
|
||||||
position: sticky; /* Make sidebar sticky */
|
position: sticky;
|
||||||
top: 100px; /* Adjust based on header height */
|
top: 100px;
|
||||||
max-height: calc(100vh - 120px); /* Adjust based on header/footer */
|
max-height: calc(100vh - 120px);
|
||||||
overflow-y: auto;
|
overflow-y: hidden;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sidebar::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-sidebar {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu ul {
|
.sidebar-menu ul {
|
||||||
@ -162,6 +172,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-table th,
|
.file-list-table th,
|
||||||
@ -181,6 +192,9 @@
|
|||||||
|
|
||||||
.file-list-table td {
|
.file-list-table td {
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-table tr:last-child td {
|
.file-list-table tr:last-child td {
|
||||||
@ -200,6 +214,43 @@
|
|||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-list-table a {
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:first-child,
|
||||||
|
.file-list-table td:first-child {
|
||||||
|
width: 40px;
|
||||||
|
max-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:nth-child(2),
|
||||||
|
.file-list-table td:nth-child(2) {
|
||||||
|
width: 40%;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:nth-child(3),
|
||||||
|
.file-list-table td:nth-child(3) {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:nth-child(4),
|
||||||
|
.file-list-table td:nth-child(4) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:nth-child(5),
|
||||||
|
.file-list-table td:nth-child(5) {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:last-child,
|
||||||
|
.file-list-table td:last-child {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive adjustments */
|
/* Responsive adjustments */
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.dashboard-layout {
|
.dashboard-layout {
|
||||||
@ -249,4 +300,8 @@
|
|||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-list-table td {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,5 +1,20 @@
|
|||||||
|
.breadcrumb {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--light-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb a {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.breadcrumb a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none !important;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
left: 0;
|
left: 0;
|
||||||
@ -10,6 +25,10 @@
|
|||||||
background-color: rgba(0, 0, 0, 0.5);
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
background-color: var(--card-background);
|
background-color: var(--card-background);
|
||||||
margin: 10% auto;
|
margin: 10% auto;
|
||||||
@ -161,6 +180,7 @@
|
|||||||
.file-list-table table {
|
.file-list-table table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-table th {
|
.file-list-table th {
|
||||||
@ -177,6 +197,9 @@
|
|||||||
padding: 12px 15px;
|
padding: 12px 15px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
max-width: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-table tr:hover {
|
.file-list-table tr:hover {
|
||||||
@ -197,12 +220,46 @@
|
|||||||
.file-list-table a {
|
.file-list-table a {
|
||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
word-break: break-all;
|
||||||
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-table a:hover {
|
.file-list-table a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-list-table th:first-child,
|
||||||
|
.file-list-table td:first-child {
|
||||||
|
width: 40px;
|
||||||
|
max-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:nth-child(2),
|
||||||
|
.file-list-table td:nth-child(2) {
|
||||||
|
width: 40%;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:nth-child(3),
|
||||||
|
.file-list-table td:nth-child(3) {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:nth-child(4),
|
||||||
|
.file-list-table td:nth-child(4) {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:nth-child(5),
|
||||||
|
.file-list-table td:nth-child(5) {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-table th:last-child,
|
||||||
|
.file-list-table td:last-child {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.modal-content {
|
.modal-content {
|
||||||
width: 95%;
|
width: 95%;
|
||||||
@ -227,6 +284,10 @@
|
|||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-list-table td {
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
.dashboard-actions {
|
.dashboard-actions {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,70 @@
|
|||||||
footer {
|
footer {
|
||||||
background-color: var(--card-background); /* Consistent with header */
|
background-color: var(--card-background);
|
||||||
color: var(--light-text-color); /* Subtle text color */
|
color: var(--light-text-color);
|
||||||
padding: 1.5rem 2rem;
|
padding: 2rem 2rem 1rem 2rem;
|
||||||
text-align: center;
|
border-top: 1px solid var(--border-color);
|
||||||
border-top: 1px solid var(--border-color); /* Separator from content */
|
margin-top: auto;
|
||||||
margin-top: auto; /* Push footer to the bottom */
|
box-shadow: 0 -2px 4px var(--shadow-color);
|
||||||
box-shadow: 0 -2px 4px var(--shadow-color); /* Subtle shadow upwards */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer p {
|
.footer-content {
|
||||||
margin: 0; /* Remove default paragraph margin */
|
display: flex;
|
||||||
font-size: 0.9rem;
|
justify-content: space-around;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto 2rem auto;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section h4 {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin: 0 0 1rem 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul li a {
|
||||||
|
color: var(--light-text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section ul li a:hover {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom {
|
||||||
|
text-align: center;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bottom p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.footer-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-section {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,135 +1,260 @@
|
|||||||
/* Styles for the Homepage (index.html) */
|
/* Hero Section */
|
||||||
|
|
||||||
.hero-section {
|
.hero-section {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 60px 20px;
|
padding: 100px 20px 80px;
|
||||||
/* Removed background-color and box-shadow to match image */
|
background: linear-gradient(135deg, #F7FAFC 0%, #FFFFFF 100%);
|
||||||
margin-bottom: 40px;
|
margin-bottom: 0;
|
||||||
position: relative;
|
}
|
||||||
overflow: hidden;
|
|
||||||
|
.hero-content {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-section h1 {
|
.hero-section h1 {
|
||||||
font-size: 3.5rem;
|
font-size: 3.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 0.5rem; /* Adjusted margin to match image */
|
margin-bottom: 1.5rem;
|
||||||
line-height: 1.2;
|
line-height: 1.15;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-section p {
|
.hero-subtitle {
|
||||||
font-size: 1.3rem;
|
font-size: 1.25rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
max-width: 800px;
|
max-width: 700px;
|
||||||
margin: 0 auto 2.5rem auto;
|
margin: 0 auto 2.5rem;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefits-grid {
|
.hero-ctas {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
gap: 30px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 40px auto; /* Adjusted margin to separate from text */
|
|
||||||
}
|
|
||||||
|
|
||||||
.benefit-card {
|
|
||||||
background-color: var(--card-background);
|
|
||||||
padding: 30px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 15px var(--shadow-color);
|
|
||||||
text-align: center;
|
|
||||||
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
gap: 1rem;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 250px; /* Ensure cards have a consistent height */
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-card:hover {
|
.hero-btn {
|
||||||
transform: translateY(-5px);
|
padding: 1rem 2rem;
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-card img.icon {
|
.hero-subtext {
|
||||||
width: 60px;
|
font-size: 0.875rem;
|
||||||
height: 60px;
|
color: var(--light-text-color);
|
||||||
margin-bottom: 20px;
|
margin: 0;
|
||||||
/* Icons in the image are colored, not inheriting text color */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-card h3 {
|
/* Features Section */
|
||||||
font-size: 1.5rem;
|
.features-section {
|
||||||
|
padding: 80px 20px;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header {
|
||||||
|
text-align: center;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header h2 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header p {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--light-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
||||||
|
gap: 2.5rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-card h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.benefit-card p {
|
.feature-card p {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--light-text-color);
|
color: var(--light-text-color);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Specific card colors from the image */
|
/* Use Cases Section */
|
||||||
.family-card {
|
.use-cases-section {
|
||||||
background-color: #D0E6F0; /* Light blue */
|
padding: 80px 20px;
|
||||||
|
background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%);
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.professional-card {
|
.use-cases-section h2 {
|
||||||
background-color: #F0E0D0; /* Light orange */
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 3rem;
|
||||||
|
color: var(--text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.student-card {
|
.use-cases-grid {
|
||||||
background-color: #D0F0D0; /* Light green */
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.find-plan-btn {
|
.use-case-card {
|
||||||
margin-top: 40px;
|
background-color: #FFFFFF;
|
||||||
padding: 15px 30px;
|
padding: 2.5rem;
|
||||||
font-size: 1.1rem;
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case-card:hover {
|
||||||
|
transform: translateY(-8px);
|
||||||
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case-icon {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case-card h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
border-radius: 5px;
|
margin-bottom: 1rem;
|
||||||
background-color: #4A90E2; /* Blue from the image */
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-case-card p {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--light-text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CTA Section */
|
||||||
|
.cta-section {
|
||||||
|
padding: 100px 20px;
|
||||||
|
background: linear-gradient(135deg, var(--primary-color) 0%, #0052CC 100%);
|
||||||
|
text-align: center;
|
||||||
color: white;
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-content {
|
||||||
|
max-width: 700px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section h2 {
|
||||||
|
font-size: 2.75rem;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section p {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: rgba(255, 255, 255, 0.9);
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-btn {
|
||||||
|
padding: 1rem 2.5rem;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background-color: white;
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-radius: 8px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transition: background-color 0.3s ease;
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.find-plan-btn:hover {
|
.cta-btn:hover {
|
||||||
background-color: #3A7BD5; /* Darker blue on hover */
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments for index.css */
|
/* Responsive Design */
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.hero-section h1 {
|
.hero-section h1 {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
}
|
}
|
||||||
.hero-section p {
|
|
||||||
font-size: 1.2rem;
|
.features-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-header h2,
|
||||||
|
.use-cases-section h2,
|
||||||
|
.cta-section h2 {
|
||||||
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.hero-section {
|
.hero-section {
|
||||||
padding: 40px 15px;
|
padding: 60px 20px 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-section h1 {
|
.hero-section h1 {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
}
|
}
|
||||||
.hero-section p {
|
|
||||||
font-size: 1rem;
|
.hero-subtitle {
|
||||||
|
font-size: 1.125rem;
|
||||||
}
|
}
|
||||||
.benefits-grid {
|
|
||||||
grid-template-columns: 1fr; /* Stack cards on small screens */
|
.hero-ctas {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
.benefit-card {
|
|
||||||
padding: 25px;
|
.hero-btn {
|
||||||
}
|
|
||||||
.find-plan-btn {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.use-cases-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.features-section,
|
||||||
|
.use-cases-section {
|
||||||
|
padding: 60px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-section {
|
||||||
|
padding: 60px 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,11 +262,14 @@
|
|||||||
.hero-section h1 {
|
.hero-section h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
.hero-section p {
|
|
||||||
font-size: 0.9rem;
|
.hero-subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
.hero-ctas .btn-primary, .hero-ctas .btn-outline {
|
|
||||||
width: 100%;
|
.features-header h2,
|
||||||
box-sizing: border-box;
|
.use-cases-section h2,
|
||||||
|
.cta-section h2 {
|
||||||
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
export function showUploadModal() {
|
export function showUploadModal() {
|
||||||
document.getElementById('upload-modal').style.display = 'block';
|
const modal = document.getElementById('upload-modal');
|
||||||
// Clear previous selections and progress
|
if (modal) {
|
||||||
|
modal.classList.add('show');
|
||||||
|
}
|
||||||
document.getElementById('selected-files-preview').innerHTML = '';
|
document.getElementById('selected-files-preview').innerHTML = '';
|
||||||
document.getElementById('upload-progress-container').innerHTML = '';
|
document.getElementById('upload-progress-container').innerHTML = '';
|
||||||
document.getElementById('file-input-multiple').value = ''; // Clear selected files
|
document.getElementById('file-input-multiple').value = '';
|
||||||
document.getElementById('start-upload-btn').disabled = true;
|
document.getElementById('start-upload-btn').disabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,6 +15,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const startUploadBtn = document.getElementById('start-upload-btn');
|
const startUploadBtn = document.getElementById('start-upload-btn');
|
||||||
const uploadProgressContainer = document.getElementById('upload-progress-container');
|
const uploadProgressContainer = document.getElementById('upload-progress-container');
|
||||||
|
|
||||||
|
if (!fileInput || !selectedFilesPreview || !startUploadBtn || !uploadProgressContainer) {
|
||||||
|
console.error('Upload elements not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let filesToUpload = [];
|
let filesToUpload = [];
|
||||||
|
|
||||||
fileInput.addEventListener('change', (event) => {
|
fileInput.addEventListener('change', (event) => {
|
||||||
@ -55,13 +62,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function uploadFiles(files) {
|
async function uploadFiles(files) {
|
||||||
startUploadBtn.disabled = true; // Disable button during upload
|
startUploadBtn.disabled = true;
|
||||||
uploadProgressContainer.innerHTML = ''; // Clear previous progress
|
uploadProgressContainer.innerHTML = '';
|
||||||
|
|
||||||
const currentPath = new URLSearchParams(window.location.search).get('path') || '';
|
const currentPath = new URLSearchParams(window.location.search).get('path') || '';
|
||||||
|
console.log('Uploading to directory:', currentPath || '(root)');
|
||||||
|
|
||||||
for (const file of files) {
|
let completedUploads = 0;
|
||||||
|
let totalFiles = files.length;
|
||||||
|
let hasErrors = false;
|
||||||
|
|
||||||
|
const uploadPromises = files.map(file => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
formData.append('current_path', currentPath);
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
|
|
||||||
const progressBarContainer = document.createElement('div');
|
const progressBarContainer = document.createElement('div');
|
||||||
@ -76,41 +90,66 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
uploadProgressContainer.appendChild(progressBarContainer);
|
uploadProgressContainer.appendChild(progressBarContainer);
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
const xhr = new XMLHttpRequest();
|
||||||
xhr.open('POST', `/files/upload?current_path=${encodeURIComponent(currentPath)}`, true);
|
xhr.open('POST', `/files/upload`, true);
|
||||||
|
|
||||||
xhr.upload.addEventListener('progress', (event) => {
|
xhr.upload.addEventListener('progress', (event) => {
|
||||||
if (event.lengthComputable) {
|
if (event.lengthComputable) {
|
||||||
const percent = (event.loaded / event.total) * 100;
|
const percent = (event.loaded / event.total) * 100;
|
||||||
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `${percent}%`;
|
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
|
||||||
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `${Math.round(percent)}%`;
|
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
|
||||||
|
if (progressBar) progressBar.style.width = `${percent}%`;
|
||||||
|
if (progressText) progressText.textContent = `${Math.round(percent)}%`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener('load', () => {
|
xhr.addEventListener('load', () => {
|
||||||
if (xhr.status === 200) {
|
if (xhr.status === 200) {
|
||||||
console.log(`File ${file.name} uploaded successfully.`);
|
console.log(`File ${file.name} uploaded successfully.`);
|
||||||
// Update progress to 100% on completion
|
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
|
||||||
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.width = `100%`;
|
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
|
||||||
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `100% (Done)`;
|
if (progressBar) progressBar.style.width = '100%';
|
||||||
|
if (progressText) progressText.textContent = '100% (Done)';
|
||||||
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
console.error(`Error uploading ${file.name}: ${xhr.statusText}`);
|
console.error(`Error uploading ${file.name}: ${xhr.statusText}`);
|
||||||
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Failed (${xhr.status})`;
|
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
|
||||||
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
|
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
|
||||||
|
if (progressText) progressText.textContent = `Failed (${xhr.status})`;
|
||||||
|
if (progressBar) progressBar.style.backgroundColor = 'red';
|
||||||
|
hasErrors = true;
|
||||||
|
reject(new Error(`Upload failed with status ${xhr.status}`));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.addEventListener('error', () => {
|
xhr.addEventListener('error', () => {
|
||||||
console.error(`Network error uploading ${file.name}.`);
|
console.error(`Network error uploading ${file.name}.`);
|
||||||
document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`).textContent = `Network Error`;
|
const progressText = document.getElementById(`progress-text-${file.name.replace(/\./g, '-')}`);
|
||||||
document.getElementById(`progress-${file.name.replace(/\./g, '-')}`).style.backgroundColor = `red`;
|
const progressBar = document.getElementById(`progress-${file.name.replace(/\./g, '-')}`);
|
||||||
|
if (progressText) progressText.textContent = 'Network Error';
|
||||||
|
if (progressBar) progressBar.style.backgroundColor = 'red';
|
||||||
|
hasErrors = true;
|
||||||
|
reject(new Error('Network error'));
|
||||||
});
|
});
|
||||||
|
|
||||||
xhr.send(formData);
|
xhr.send(formData);
|
||||||
}
|
});
|
||||||
// After all files are sent, refresh the page to show new files
|
});
|
||||||
// A small delay to allow server to process and update file list
|
|
||||||
|
try {
|
||||||
|
await Promise.allSettled(uploadPromises);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.reload();
|
const currentUrl = new URL(window.location.href);
|
||||||
}, 1000);
|
const pathParam = currentUrl.searchParams.get('path');
|
||||||
|
if (pathParam) {
|
||||||
|
window.location.href = `/files?path=${encodeURIComponent(pathParam)}`;
|
||||||
|
} else {
|
||||||
|
window.location.href = '/files';
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during upload:', error);
|
||||||
|
startUploadBtn.disabled = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -88,17 +88,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
// Helper functions for modals
|
// Helper functions for modals
|
||||||
function showNewFolderModal() {
|
function showNewFolderModal() {
|
||||||
document.getElementById('new-folder-modal').style.display = 'block';
|
const modal = document.getElementById('new-folder-modal');
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add('show');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal(modalId) {
|
function closeModal(modalId) {
|
||||||
document.getElementById(modalId).style.display = 'none';
|
const modal = document.getElementById(modalId);
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.remove('show');
|
||||||
}
|
}
|
||||||
window.closeModal = closeModal; // Make it globally accessible
|
}
|
||||||
|
window.closeModal = closeModal;
|
||||||
|
|
||||||
window.onclick = function(event) {
|
window.onclick = function(event) {
|
||||||
if (event.target.classList.contains('modal')) {
|
if (event.target.classList.contains('modal')) {
|
||||||
event.target.style.display = 'none';
|
event.target.classList.remove('show');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,14 +119,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const loading = document.getElementById('share-loading');
|
const loading = document.getElementById('share-loading');
|
||||||
const shareLinkInput = document.getElementById('share-link-input');
|
const shareLinkInput = document.getElementById('share-link-input');
|
||||||
const shareFileName = document.getElementById('share-file-name');
|
const shareFileName = document.getElementById('share-file-name');
|
||||||
const shareLinksList = document.getElementById('share-links-list'); // New element for multiple links
|
const shareLinksList = document.getElementById('share-links-list');
|
||||||
|
|
||||||
// Clear previous content
|
|
||||||
shareLinkInput.value = '';
|
shareLinkInput.value = '';
|
||||||
if (shareLinksList) shareLinksList.innerHTML = '';
|
if (shareLinksList) shareLinksList.innerHTML = '';
|
||||||
linkContainer.style.display = 'none';
|
linkContainer.style.display = 'none';
|
||||||
loading.style.display = 'block';
|
loading.style.display = 'block';
|
||||||
modal.style.display = 'block';
|
modal.classList.add('show');
|
||||||
|
|
||||||
if (paths.length === 1) {
|
if (paths.length === 1) {
|
||||||
shareFileName.textContent = `Sharing: ${names[0]}`;
|
shareFileName.textContent = `Sharing: ${names[0]}`;
|
||||||
@ -180,7 +185,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const deleteMessage = document.getElementById('delete-message');
|
const deleteMessage = document.getElementById('delete-message');
|
||||||
const deleteModal = document.getElementById('delete-modal');
|
const deleteModal = document.getElementById('delete-modal');
|
||||||
|
|
||||||
// Clear previous hidden inputs
|
|
||||||
deleteForm.querySelectorAll('input[name="paths[]"]').forEach(input => input.remove());
|
deleteForm.querySelectorAll('input[name="paths[]"]').forEach(input => input.remove());
|
||||||
|
|
||||||
if (Array.isArray(paths) && paths.length > 1) {
|
if (Array.isArray(paths) && paths.length > 1) {
|
||||||
@ -199,7 +203,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
deleteMessage.textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
|
deleteMessage.textContent = `Are you sure you want to delete "${name}"? This action cannot be undone.`;
|
||||||
deleteForm.action = `/files/delete/${path}`;
|
deleteForm.action = `/files/delete/${path}`;
|
||||||
}
|
}
|
||||||
deleteModal.style.display = 'block';
|
deleteModal.classList.add('show');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Selection and action buttons
|
// Selection and action buttons
|
||||||
|
|||||||
228
retoors/templates/components/cookie_banner.html
Normal file
228
retoors/templates/components/cookie_banner.html
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
<div id="cookie-banner" class="cookie-banner" style="display: none;">
|
||||||
|
<div class="cookie-banner-content">
|
||||||
|
<div class="cookie-banner-text">
|
||||||
|
<h3>Cookie Settings</h3>
|
||||||
|
<p>We use cookies to provide essential website functionality and improve your experience. For more information, please read our <a href="/cookies">Cookie Policy</a>.</p>
|
||||||
|
</div>
|
||||||
|
<div class="cookie-banner-actions">
|
||||||
|
<button id="cookie-reject" class="btn-outline">Reject All</button>
|
||||||
|
<button id="cookie-customize" class="btn-outline">Customize</button>
|
||||||
|
<button id="cookie-accept" class="btn-primary">Accept All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="cookie-preferences-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close" id="close-cookie-modal">×</span>
|
||||||
|
<h3>Cookie Preferences</h3>
|
||||||
|
<p>Manage your cookie preferences below. Some cookies are essential for the website to function and cannot be disabled.</p>
|
||||||
|
|
||||||
|
<div class="cookie-category">
|
||||||
|
<div class="cookie-category-header">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="cookie-necessary" checked disabled>
|
||||||
|
<strong>Strictly Necessary Cookies</strong>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p>These cookies are essential for the website to function properly. They enable core functionality such as security and session management.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cookie-category">
|
||||||
|
<div class="cookie-category-header">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="cookie-functional">
|
||||||
|
<strong>Functional Cookies</strong>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p>These cookies allow the website to remember your preferences and provide enhanced features.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cookie-category">
|
||||||
|
<div class="cookie-category-header">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" id="cookie-analytics">
|
||||||
|
<strong>Analytics Cookies</strong>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p>These cookies help us understand how visitors interact with our website by collecting and reporting information anonymously.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="save-cookie-preferences" class="btn-primary">Save Preferences</button>
|
||||||
|
<button id="cancel-cookie-preferences" class="btn-outline">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cookie-banner {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: var(--card-background);
|
||||||
|
border-top: 2px solid var(--border-color);
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-banner-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-banner-text {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-banner-text h3 {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-banner-text p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--light-text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-banner-text a {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-banner-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-category {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-category-header {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-category-header label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-category-header input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-category p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--light-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.cookie-banner-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-banner-actions {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cookie-banner-actions button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const banner = document.getElementById('cookie-banner');
|
||||||
|
const acceptBtn = document.getElementById('cookie-accept');
|
||||||
|
const rejectBtn = document.getElementById('cookie-reject');
|
||||||
|
const customizeBtn = document.getElementById('cookie-customize');
|
||||||
|
const modal = document.getElementById('cookie-preferences-modal');
|
||||||
|
const closeModal = document.getElementById('close-cookie-modal');
|
||||||
|
const savePreferences = document.getElementById('save-cookie-preferences');
|
||||||
|
const cancelPreferences = document.getElementById('cancel-cookie-preferences');
|
||||||
|
|
||||||
|
function getCookieConsent() {
|
||||||
|
return localStorage.getItem('cookie-consent');
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookieConsent(value) {
|
||||||
|
localStorage.setItem('cookie-consent', value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookiePreferences() {
|
||||||
|
const prefs = localStorage.getItem('cookie-preferences');
|
||||||
|
return prefs ? JSON.parse(prefs) : { necessary: true, functional: false, analytics: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookiePreferences(prefs) {
|
||||||
|
localStorage.setItem('cookie-preferences', JSON.stringify(prefs));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!getCookieConsent()) {
|
||||||
|
banner.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptBtn.addEventListener('click', function() {
|
||||||
|
setCookieConsent('accepted');
|
||||||
|
setCookiePreferences({ necessary: true, functional: true, analytics: true });
|
||||||
|
banner.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
rejectBtn.addEventListener('click', function() {
|
||||||
|
setCookieConsent('rejected');
|
||||||
|
setCookiePreferences({ necessary: true, functional: false, analytics: false });
|
||||||
|
banner.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
customizeBtn.addEventListener('click', function() {
|
||||||
|
const prefs = getCookiePreferences();
|
||||||
|
document.getElementById('cookie-functional').checked = prefs.functional;
|
||||||
|
document.getElementById('cookie-analytics').checked = prefs.analytics;
|
||||||
|
modal.style.display = 'block';
|
||||||
|
});
|
||||||
|
|
||||||
|
closeModal.addEventListener('click', function() {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelPreferences.addEventListener('click', function() {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
savePreferences.addEventListener('click', function() {
|
||||||
|
const prefs = {
|
||||||
|
necessary: true,
|
||||||
|
functional: document.getElementById('cookie-functional').checked,
|
||||||
|
analytics: document.getElementById('cookie-analytics').checked
|
||||||
|
};
|
||||||
|
setCookieConsent('customized');
|
||||||
|
setCookiePreferences(prefs);
|
||||||
|
modal.style.display = 'none';
|
||||||
|
banner.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('click', function(event) {
|
||||||
|
if (event.target === modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -1,3 +1,31 @@
|
|||||||
<footer aria-label="Site information and copyright">
|
<footer aria-label="Site information and copyright">
|
||||||
|
<div class="footer-content">
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>Legal</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/privacy">Privacy Policy</a></li>
|
||||||
|
<li><a href="/cookies">Cookie Policy</a></li>
|
||||||
|
<li><a href="/terms">Terms of Service</a></li>
|
||||||
|
<li><a href="/impressum">Impressum</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>Policies</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/aup">Acceptable Use Policy</a></li>
|
||||||
|
<li><a href="/sla">Service Level Agreement</a></li>
|
||||||
|
<li><a href="/compliance">Security & Compliance</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="footer-section">
|
||||||
|
<h4>User Rights</h4>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/user_rights">Data Access & Deletion</a></li>
|
||||||
|
<li><a href="/support">Contact Support</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer-bottom">
|
||||||
<p>© 2025 Retoors. All rights reserved.</p>
|
<p>© 2025 Retoors. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -1,22 +1,26 @@
|
|||||||
<header>
|
<header class="site-header">
|
||||||
<nav aria-label="Main navigation">
|
<nav class="site-nav" aria-label="Main navigation">
|
||||||
<a href="/" class="logo" aria-label="HomeBase Storage home">
|
<div class="nav-container">
|
||||||
<img src="/static/images/retoors-logo.svg" alt="HomeBase Storage" />
|
<a href="/" class="brand" aria-label="Retoor's Cloud Solutions home">
|
||||||
<span>HomeBase Storage</span>
|
<span class="brand-text">Retoor's</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="nav-links">
|
<ul class="nav-menu">
|
||||||
<li><a href="/solutions" aria-label="Our Solutions">Solutions</a></li>
|
<li><a href="/solutions">Solutions</a></li>
|
||||||
<li><a href="/pricing" aria-label="Pricing Plans">Pricing</a></li>
|
<li><a href="/pricing">Pricing</a></li>
|
||||||
<li><a href="/security" aria-label="Security Information">Security</a></li>
|
<li><a href="/security">Security</a></li>
|
||||||
<li><a href="/support" aria-label="Support Page">Support</a></li>
|
|
||||||
{% if request['user'] %}
|
{% if request['user'] %}
|
||||||
<li><a href="/dashboard" aria-label="User Dashboard">Dashboard</a></li>
|
<li><a href="/files">My Files</a></li>
|
||||||
<li><a href="/files" aria-label="File Browser">File Browser</a></li>
|
|
||||||
<li><a href="/logout" class="btn-primary-nav" aria-label="Logout">Logout</a></li>
|
|
||||||
{% else %}
|
|
||||||
<li><a href="/login" class="btn-outline-nav" aria-label="Sign In to your account">Sign In</a></li>
|
|
||||||
<li><a href="/register" class="btn-primary-nav" aria-label="Start your free trial">Start Your Free Trial</a></li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
<div class="nav-actions">
|
||||||
|
{% if request['user'] %}
|
||||||
|
<a href="/files" class="nav-link">Dashboard</a>
|
||||||
|
<a href="/logout" class="btn-outline">Logout</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="/login" class="nav-link">Sign In</a>
|
||||||
|
<a href="/register" class="btn-primary">Get Started Free</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@ -3,18 +3,17 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}Retoors Storage{% endblock %}</title>
|
<title>{% block title %}Retoors Cloud Solutions{% endblock %}</title>
|
||||||
<link rel="stylesheet" href="/static/css/base.css">
|
<link rel="stylesheet" href="/static/css/base.css">
|
||||||
<link rel="stylesheet" href="/static/css/components/footer.css">
|
<link rel="stylesheet" href="/static/css/components/footer.css">
|
||||||
<link rel="stylesheet" href="/static/css/components/content_pages.css"> {# Added for content page styling #}
|
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% include 'components/navigation.html' %}
|
||||||
<div class="container">
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
|
||||||
{% include 'components/footer.html' %}
|
{% include 'components/footer.html' %}
|
||||||
|
{% include 'components/cookie_banner.html' %}
|
||||||
<script src="/static/js/main.js" type="module"></script>
|
<script src="/static/js/main.js" type="module"></script>
|
||||||
{% block scripts %}{% endblock %}
|
{% block scripts %}{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
94
retoors/templates/pages/aup.html
Normal file
94
retoors/templates/pages/aup.html
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Acceptable Use Policy{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<section class="content-section">
|
||||||
|
<h1>Acceptable Use Policy</h1>
|
||||||
|
<p>Last updated: January 2025</p>
|
||||||
|
<p>This Acceptable Use Policy governs your use of Retoor's Cloud Solutions services. By using our services, you agree to comply with this policy.</p>
|
||||||
|
|
||||||
|
<h2>1. Prohibited Content</h2>
|
||||||
|
<p>You may not use our services to store, share, or distribute content that:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Is illegal under Dutch or EU law</li>
|
||||||
|
<li>Contains malware, viruses, or other harmful code</li>
|
||||||
|
<li>Infringes intellectual property rights, including copyright, trademark, or patent rights</li>
|
||||||
|
<li>Contains child sexual abuse material or exploitation content</li>
|
||||||
|
<li>Promotes terrorism, violence, or hatred</li>
|
||||||
|
<li>Contains personal data of others without proper authorization</li>
|
||||||
|
<li>Is defamatory, fraudulent, or deceptive</li>
|
||||||
|
<li>Violates the privacy or data protection rights of others</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>2. Prohibited Activities</h2>
|
||||||
|
<p>You may not use our services to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Attempt to gain unauthorized access to our systems or other users' accounts</li>
|
||||||
|
<li>Interfere with or disrupt the integrity or performance of our services</li>
|
||||||
|
<li>Attempt to decipher, decompile, or reverse engineer any software comprising our services</li>
|
||||||
|
<li>Engage in any form of automated data collection or scraping</li>
|
||||||
|
<li>Use our services to send spam, phishing attempts, or other unsolicited communications</li>
|
||||||
|
<li>Resell or redistribute our services without authorization</li>
|
||||||
|
<li>Use our services for cryptocurrency mining</li>
|
||||||
|
<li>Engage in activities that could harm our reputation or business operations</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>3. Resource Usage Limits</h2>
|
||||||
|
<p>Your use of our services is subject to the following limits:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Storage quota as defined in your service plan</li>
|
||||||
|
<li>Reasonable bandwidth usage consistent with normal cloud storage operations</li>
|
||||||
|
<li>Maximum file size limits as specified in your plan</li>
|
||||||
|
<li>API rate limits to ensure fair usage for all users</li>
|
||||||
|
</ul>
|
||||||
|
<p>Excessive resource usage that impacts service performance for other users may result in throttling or suspension.</p>
|
||||||
|
|
||||||
|
<h2>4. Security Requirements</h2>
|
||||||
|
<p>You are responsible for:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Maintaining the confidentiality of your account credentials</li>
|
||||||
|
<li>Promptly notifying us of any unauthorized access to your account</li>
|
||||||
|
<li>Using strong passwords and enabling two-factor authentication when available</li>
|
||||||
|
<li>Ensuring that any data you upload does not contain malware or viruses</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>5. Consequences of Violation</h2>
|
||||||
|
<p>Violation of this Acceptable Use Policy may result in:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Immediate removal of prohibited content</li>
|
||||||
|
<li>Temporary suspension of your account</li>
|
||||||
|
<li>Permanent termination of your account and services</li>
|
||||||
|
<li>Referral to law enforcement authorities</li>
|
||||||
|
<li>Legal action to recover damages</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>6. Reporting Abuse</h2>
|
||||||
|
<p>If you become aware of any violation of this policy, please report it to us immediately:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Email: abuse@retoors.nl</li>
|
||||||
|
<li>Subject line: "AUP Violation Report"</li>
|
||||||
|
<li>Include: Details of the violation, relevant URLs or account information, and any supporting evidence</li>
|
||||||
|
</ul>
|
||||||
|
<p>We will investigate all reports and take appropriate action within a reasonable timeframe.</p>
|
||||||
|
|
||||||
|
<h2>7. Investigation Rights</h2>
|
||||||
|
<p>We reserve the right to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Investigate suspected violations of this policy</li>
|
||||||
|
<li>Access and review content stored on our services when necessary to ensure compliance</li>
|
||||||
|
<li>Cooperate with law enforcement authorities</li>
|
||||||
|
<li>Remove content or suspend accounts pending investigation</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>8. Modifications to This Policy</h2>
|
||||||
|
<p>We may modify this Acceptable Use Policy at any time. Continued use of our services after modifications constitutes acceptance of the updated policy.</p>
|
||||||
|
|
||||||
|
<h2>9. Questions</h2>
|
||||||
|
<p>If you have questions about this policy, please contact us at: <a href="mailto:legal@retoors.nl">legal@retoors.nl</a></p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
194
retoors/templates/pages/compliance.html
Normal file
194
retoors/templates/pages/compliance.html
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Security & Compliance{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<section class="content-section">
|
||||||
|
<h1>Security & Compliance</h1>
|
||||||
|
<p>Retoor's Cloud Solutions is committed to maintaining the highest standards of security and compliance to protect your data and ensure regulatory adherence.</p>
|
||||||
|
|
||||||
|
<h2>1. Data Security Measures</h2>
|
||||||
|
|
||||||
|
<h3>1.1 Encryption Standards</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Standard</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Data in Transit</td>
|
||||||
|
<td>TLS 1.3</td>
|
||||||
|
<td>All data transmitted between your device and our servers is encrypted using the latest TLS protocol</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Data at Rest</td>
|
||||||
|
<td>AES-256</td>
|
||||||
|
<td>All stored files are encrypted using industry-standard AES-256 encryption</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Database</td>
|
||||||
|
<td>AES-256</td>
|
||||||
|
<td>User credentials and metadata are encrypted at the database level</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>1.2 Access Control</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Multi-factor authentication (MFA) available for all accounts</li>
|
||||||
|
<li>Role-based access control (RBAC) for team accounts</li>
|
||||||
|
<li>Session management with automatic timeout</li>
|
||||||
|
<li>IP whitelisting available for Enterprise customers</li>
|
||||||
|
<li>Audit logs for all file access and modifications</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>1.3 Infrastructure Security</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Data centers hosted by Hetzner in Germany and Finland (EU-based)</li>
|
||||||
|
<li>24/7 physical security and monitoring</li>
|
||||||
|
<li>DDoS protection and intrusion detection systems</li>
|
||||||
|
<li>Regular security audits and penetration testing</li>
|
||||||
|
<li>Automated backup systems with geographic redundancy</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>2. Compliance Certifications</h2>
|
||||||
|
|
||||||
|
<h3>2.1 GDPR Compliance</h3>
|
||||||
|
<p>We are fully compliant with the General Data Protection Regulation (GDPR):</p>
|
||||||
|
<ul>
|
||||||
|
<li>Data processing agreements available for all customers</li>
|
||||||
|
<li>Right to access, rectification, erasure, and portability</li>
|
||||||
|
<li>Data breach notification within 72 hours</li>
|
||||||
|
<li>Privacy by design and by default</li>
|
||||||
|
<li>All data stored within the European Union</li>
|
||||||
|
<li>No data transfers outside EU without appropriate safeguards</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>2.2 ISO 27001</h3>
|
||||||
|
<p>Our information security management system is aligned with ISO 27001 standards:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Regular risk assessments and security reviews</li>
|
||||||
|
<li>Documented security policies and procedures</li>
|
||||||
|
<li>Employee security training and awareness programs</li>
|
||||||
|
<li>Incident response and business continuity plans</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>2.3 SOC 2 Type II</h3>
|
||||||
|
<p>We maintain SOC 2 Type II compliance covering:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Security: Protection against unauthorized access</li>
|
||||||
|
<li>Availability: System uptime and performance</li>
|
||||||
|
<li>Confidentiality: Protection of sensitive information</li>
|
||||||
|
<li>Privacy: Handling of personal information</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>3. Data Center Locations</h2>
|
||||||
|
<p>Your data is stored exclusively in European Union data centers:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Provider</th>
|
||||||
|
<th>Certifications</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Falkenstein, Germany</td>
|
||||||
|
<td>Hetzner Online GmbH</td>
|
||||||
|
<td>ISO 27001, PCI DSS</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Helsinki, Finland</td>
|
||||||
|
<td>Hetzner Online GmbH</td>
|
||||||
|
<td>ISO 27001, PCI DSS</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p>All data centers feature:</p>
|
||||||
|
<ul>
|
||||||
|
<li>99.99% power availability with redundant power supplies</li>
|
||||||
|
<li>Climate-controlled environments</li>
|
||||||
|
<li>Biometric access control</li>
|
||||||
|
<li>24/7 on-site security personnel</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>4. Data Processing Agreement</h2>
|
||||||
|
<p>For business customers, we provide a comprehensive Data Processing Agreement (DPA) that includes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Clear definition of roles (Controller vs. Processor)</li>
|
||||||
|
<li>List of sub-processors and their locations</li>
|
||||||
|
<li>Data security measures and obligations</li>
|
||||||
|
<li>Data subject rights and assistance procedures</li>
|
||||||
|
<li>Data breach notification procedures</li>
|
||||||
|
<li>Terms for data deletion upon contract termination</li>
|
||||||
|
</ul>
|
||||||
|
<p><a href="/dpa" class="btn-primary">Download DPA Template</a></p>
|
||||||
|
|
||||||
|
<h2>5. Security Monitoring</h2>
|
||||||
|
<p>We continuously monitor our systems for security threats:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Real-time threat detection and alerting</li>
|
||||||
|
<li>Automated vulnerability scanning</li>
|
||||||
|
<li>Security information and event management (SIEM)</li>
|
||||||
|
<li>Regular penetration testing by third-party security firms</li>
|
||||||
|
<li>Bug bounty program for responsible disclosure</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>6. Incident Response</h2>
|
||||||
|
<p>In the event of a security incident:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Immediate containment and investigation</li>
|
||||||
|
<li>Notification to affected customers within 24 hours</li>
|
||||||
|
<li>Detailed incident reports provided to Business and Enterprise customers</li>
|
||||||
|
<li>Post-incident review and remediation</li>
|
||||||
|
<li>Cooperation with regulatory authorities as required</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>7. Employee Security</h2>
|
||||||
|
<p>All employees undergo rigorous security protocols:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Background checks for all staff with data access</li>
|
||||||
|
<li>Confidentiality and non-disclosure agreements</li>
|
||||||
|
<li>Regular security awareness training</li>
|
||||||
|
<li>Principle of least privilege access</li>
|
||||||
|
<li>Secure development practices and code reviews</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>8. Third-Party Audits</h2>
|
||||||
|
<p>We undergo regular third-party security audits:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Annual penetration testing by certified security firms</li>
|
||||||
|
<li>Quarterly vulnerability assessments</li>
|
||||||
|
<li>Independent compliance audits for ISO and SOC certifications</li>
|
||||||
|
<li>Audit reports available to Enterprise customers upon request</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>9. Security Best Practices for Users</h2>
|
||||||
|
<p>We recommend the following security practices:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Enable two-factor authentication on your account</li>
|
||||||
|
<li>Use strong, unique passwords</li>
|
||||||
|
<li>Regularly review account activity and access logs</li>
|
||||||
|
<li>Keep your contact information up to date</li>
|
||||||
|
<li>Be cautious of phishing attempts</li>
|
||||||
|
<li>Report suspicious activity immediately</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>10. Questions and Reporting</h2>
|
||||||
|
<p>For security-related inquiries or to report vulnerabilities:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Security Team: security@retoors.nl</li>
|
||||||
|
<li>Vulnerability Disclosure: security-disclosure@retoors.nl</li>
|
||||||
|
<li>Compliance Questions: compliance@retoors.nl</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
80
retoors/templates/pages/cookies.html
Normal file
80
retoors/templates/pages/cookies.html
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Cookie Policy{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<section class="content-section">
|
||||||
|
<h1>Cookie Policy</h1>
|
||||||
|
<p>Last updated: January 2025</p>
|
||||||
|
|
||||||
|
<h2>1. What Are Cookies</h2>
|
||||||
|
<p>Cookies are small text files that are placed on your device when you visit our website. They help us provide you with a better experience by remembering your preferences and understanding how you use our service.</p>
|
||||||
|
|
||||||
|
<h2>2. Cookie Categories</h2>
|
||||||
|
|
||||||
|
<h3>2.1 Strictly Necessary Cookies</h3>
|
||||||
|
<p>These cookies are essential for the website to function properly. They enable core functionality such as security, network management, and accessibility. You cannot opt out of these cookies as they are required for the service to work.</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Provider</th>
|
||||||
|
<th>Purpose</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>session</td>
|
||||||
|
<td>Retoor's Cloud Solutions</td>
|
||||||
|
<td>Maintains your login session</td>
|
||||||
|
<td>Session</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>2.2 Functional Cookies</h3>
|
||||||
|
<p>These cookies allow us to remember choices you make and provide enhanced, more personalized features.</p>
|
||||||
|
|
||||||
|
<h3>2.3 Analytics Cookies</h3>
|
||||||
|
<p>These cookies help us understand how visitors interact with our website by collecting and reporting information anonymously. We use this data to improve our service.</p>
|
||||||
|
|
||||||
|
<h3>2.4 Marketing/Tracking Cookies</h3>
|
||||||
|
<p>We currently do not use marketing or tracking cookies.</p>
|
||||||
|
|
||||||
|
<h2>3. Third-Party Cookies</h2>
|
||||||
|
<p>We may use third-party services that set cookies on our behalf. These services include:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Payment processors for handling transactions</li>
|
||||||
|
<li>Hosting providers (Hetzner) for infrastructure services</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>4. Legal Basis</h2>
|
||||||
|
<p>We use strictly necessary cookies based on our legitimate interest in providing a functional service. For all other cookies, we obtain your explicit consent before placing them on your device.</p>
|
||||||
|
|
||||||
|
<h2>5. How to Manage Cookies</h2>
|
||||||
|
<p>You can control and manage cookies in several ways:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Browser settings: Most browsers allow you to refuse or accept cookies through their settings. Please note that disabling cookies may impact your experience on our website.</li>
|
||||||
|
<li>Cookie preferences: You can manage your cookie preferences using our cookie consent banner.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Browser-Specific Instructions</h3>
|
||||||
|
<ul>
|
||||||
|
<li>Chrome: Settings > Privacy and security > Cookies and other site data</li>
|
||||||
|
<li>Firefox: Settings > Privacy & Security > Cookies and Site Data</li>
|
||||||
|
<li>Safari: Preferences > Privacy > Cookies and website data</li>
|
||||||
|
<li>Edge: Settings > Privacy, search, and services > Cookies and site permissions</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>6. Changes to This Policy</h2>
|
||||||
|
<p>We may update this Cookie Policy from time to time. Any changes will be posted on this page with an updated revision date.</p>
|
||||||
|
|
||||||
|
<h2>7. Contact Us</h2>
|
||||||
|
<p>If you have any questions about our use of cookies, please contact us through our support page.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@ -1,12 +1,12 @@
|
|||||||
{% extends "layouts/dashboard.html" %}
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
{% block title %}My Files - Retoor's Cloud Solutions{% endblock %}
|
{% block title %}{% if current_path %}{{ current_path.split('/')[-1] }} - Retoor's Cloud Solutions{% else %}My Files - Retoor's Cloud Solutions{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block dashboard_head %}
|
{% 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 page_title %}My Files{% endblock %}
|
{% block page_title %}{% if current_path %}{{ current_path.split('/')[-1] }}{% else %}My Files{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block dashboard_actions %}
|
{% block dashboard_actions %}
|
||||||
<button class="btn-primary" id="new-folder-btn">+ New</button>
|
<button class="btn-primary" id="new-folder-btn">+ New</button>
|
||||||
@ -55,8 +55,14 @@
|
|||||||
<a href="/files?path={{ item.path }}">{{ item.name }}</a>
|
<a href="/files?path={{ item.path }}">{{ item.name }}</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
|
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
|
||||||
|
{% if item.is_editable %}
|
||||||
|
<a href="/editor?path={{ item.path }}">{{ item.name }}</a>
|
||||||
|
{% elif item.is_viewable %}
|
||||||
|
<a href="/viewer?path={{item.path}}">{{ item.name }}</a>
|
||||||
|
{% else %}
|
||||||
{{ item.name }}
|
{{ item.name }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ user.email }}</td>
|
<td>{{ user.email }}</td>
|
||||||
<td>{{ item.last_modified[:10] }}</td>
|
<td>{{ item.last_modified[:10] }}</td>
|
||||||
|
|||||||
159
retoors/templates/pages/file_editor.html
Normal file
159
retoors/templates/pages/file_editor.html
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ file_path.split('/')[-1] }} - Editor - Retoor's Cloud Solutions{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_head %}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css">
|
||||||
|
<style>
|
||||||
|
.editor-container {
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.editor-toolbar {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.editor-toolbar .file-info {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.editor-toolbar .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.CodeMirror {
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
font-size: 14px;
|
||||||
|
background: white !important;
|
||||||
|
}
|
||||||
|
.save-status {
|
||||||
|
margin-left: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_title %}Editing: {{ file_path }}{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_actions %}
|
||||||
|
<a href="/files?path={{ file_path.rsplit('/', 1)[0] }}" class="btn-outline">Back to Files</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_content %}
|
||||||
|
<div class="editor-container">
|
||||||
|
<div class="editor-toolbar">
|
||||||
|
<div class="file-info">
|
||||||
|
{{ file_path }}
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="save-btn" class="btn-primary">Save</button>
|
||||||
|
<span id="save-status" class="save-status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="editor-textarea"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/javascript/javascript.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/python/python.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/htmlmixed/htmlmixed.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/css/css.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/markdown/markdown.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/xml/xml.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/shell/shell.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/yaml/yaml.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/edit/closebrackets.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/addon/edit/matchbrackets.min.js"></script>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const textarea = document.getElementById('editor-textarea');
|
||||||
|
const saveBtn = document.getElementById('save-btn');
|
||||||
|
const saveStatus = document.getElementById('save-status');
|
||||||
|
const filePath = '{{ file_path }}';
|
||||||
|
|
||||||
|
// Determine mode based on file extension
|
||||||
|
const fileName = filePath.split('/').pop();
|
||||||
|
const ext = fileName.split('.').pop().toLowerCase();
|
||||||
|
let mode = null;
|
||||||
|
if (['js', 'mjs', 'json'].includes(ext)) mode = 'javascript';
|
||||||
|
else if (ext === 'py') mode = 'python';
|
||||||
|
else if (['html', 'htm'].includes(ext)) mode = 'htmlmixed';
|
||||||
|
else if (ext === 'css') mode = 'css';
|
||||||
|
else if (['md', 'markdown'].includes(ext)) mode = 'markdown';
|
||||||
|
else if (['xml', 'svg'].includes(ext)) mode = 'xml';
|
||||||
|
else if (['sh', 'bash'].includes(ext)) mode = 'shell';
|
||||||
|
else if (['yml', 'yaml'].includes(ext)) mode = 'yaml';
|
||||||
|
|
||||||
|
// Initialize CodeMirror
|
||||||
|
const editor = CodeMirror.fromTextArea(textarea, {
|
||||||
|
lineNumbers: true,
|
||||||
|
mode: mode,
|
||||||
|
theme: 'default',
|
||||||
|
indentUnit: 4,
|
||||||
|
tabSize: 4,
|
||||||
|
indentWithTabs: false,
|
||||||
|
autoCloseBrackets: true,
|
||||||
|
matchBrackets: true,
|
||||||
|
lineWrapping: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load file content
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/file/content?path=${encodeURIComponent(filePath)}`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.status === 'success') {
|
||||||
|
editor.setValue(data.content);
|
||||||
|
} else {
|
||||||
|
alert('Error loading file: ' + data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('Error loading file: ' + error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save functionality
|
||||||
|
saveBtn.addEventListener('click', async () => {
|
||||||
|
const content = editor.getValue();
|
||||||
|
saveStatus.textContent = 'Saving...';
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/file/save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
path: filePath,
|
||||||
|
content: content
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.status === 'success') {
|
||||||
|
saveStatus.textContent = 'Saved successfully';
|
||||||
|
setTimeout(() => saveStatus.textContent = '', 2000);
|
||||||
|
} else {
|
||||||
|
saveStatus.textContent = 'Save failed: ' + data.message;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
saveStatus.textContent = 'Save failed: ' + error.message;
|
||||||
|
} finally {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-save on Ctrl+S
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
||||||
|
e.preventDefault();
|
||||||
|
saveBtn.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
67
retoors/templates/pages/impressum.html
Normal file
67
retoors/templates/pages/impressum.html
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Impressum{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<section class="content-section">
|
||||||
|
<h1>Impressum</h1>
|
||||||
|
<p>Information in accordance with Dutch law requirements</p>
|
||||||
|
|
||||||
|
<h2>Company Information</h2>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Company Name:</strong></td>
|
||||||
|
<td>Retoor's Cloud Solutions</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Legal Form:</strong></td>
|
||||||
|
<td>Sole Proprietorship</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>KvK Number:</strong></td>
|
||||||
|
<td>[To be filled in]</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>BTW/VAT Number:</strong></td>
|
||||||
|
<td>[To be filled in]</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Contact Information</h2>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Business Address:</strong></td>
|
||||||
|
<td>[Street Address]<br>[Postal Code] [City]<br>The Netherlands</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Email:</strong></td>
|
||||||
|
<td>contact@retoors.nl</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Phone:</strong></td>
|
||||||
|
<td>[Phone Number]</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>Dispute Resolution</h2>
|
||||||
|
<p>The European Commission provides a platform for online dispute resolution (ODR): <a href="https://ec.europa.eu/consumers/odr" target="_blank">https://ec.europa.eu/consumers/odr</a></p>
|
||||||
|
<p>We are not willing or obliged to participate in dispute resolution proceedings before a consumer arbitration board.</p>
|
||||||
|
|
||||||
|
<h2>Liability for Content</h2>
|
||||||
|
<p>As service providers, we are liable for own contents of these websites according to general laws. However, we are not obliged to monitor external information provided or stored on our website. Once we have become aware of a specific infringement of the law, we will immediately remove the content in question.</p>
|
||||||
|
|
||||||
|
<h2>Liability for Links</h2>
|
||||||
|
<p>Our website contains links to external websites, over whose contents we have no control. Therefore, we cannot accept any liability for these external contents. The respective provider or operator of the websites is always responsible for the contents of the linked websites.</p>
|
||||||
|
|
||||||
|
<h2>Copyright</h2>
|
||||||
|
<p>The contents and works on these pages created by the site operators are subject to Dutch copyright law. The duplication, processing, distribution and any kind of utilization outside the limits of copyright law require the written consent of the respective author or creator.</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
{% extends "layouts/base.html" %}
|
{% extends "layouts/base.html" %}
|
||||||
|
|
||||||
{% block title %}Solutions for Everyone - Retoor's Cloud Solutions{% endblock %}
|
{% block title %}Secure Cloud Storage for Everyone - Retoor's Cloud Solutions{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<link rel="stylesheet" href="/static/css/components/index.css">
|
<link rel="stylesheet" href="/static/css/components/index.css">
|
||||||
@ -9,26 +9,114 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
<section class="hero-section">
|
<section class="hero-section">
|
||||||
<h1>Solutions for Everyone</h1>
|
<div class="hero-content">
|
||||||
<p>Solutions for Everyone</p>
|
<h1>Your files, safe and accessible everywhere</h1>
|
||||||
<div class="benefits-grid">
|
<p class="hero-subtitle">Store, sync, and share your files with enterprise-grade security. Access from any device, anytime, anywhere.</p>
|
||||||
<div class="benefit-card family-card">
|
<div class="hero-ctas">
|
||||||
<img src="/static/images/icon-families.svg" alt="Families Icon" class="icon">
|
<a href="/register" class="btn-primary hero-btn">Get Started Free</a>
|
||||||
|
<a href="/pricing" class="btn-outline hero-btn">View Pricing</a>
|
||||||
|
</div>
|
||||||
|
<p class="hero-subtext">No credit card required • 10 GB free storage</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="features-section">
|
||||||
|
<div class="features-header">
|
||||||
|
<h2>Everything you need to work smarter</h2>
|
||||||
|
<p>Powerful features designed to keep your data secure and accessible</p>
|
||||||
|
</div>
|
||||||
|
<div class="features-grid">
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>Bank-level Security</h3>
|
||||||
|
<p>256-bit AES encryption and TLS 1.3 protocol ensure your files stay private and protected</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="2" y="7" width="20" height="14" rx="2" ry="2"/>
|
||||||
|
<path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>Access Anywhere</h3>
|
||||||
|
<p>Seamlessly sync across all your devices. Desktop, mobile, or web - your files are always there</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
||||||
|
<circle cx="9" cy="7" r="4"/>
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>Easy Sharing</h3>
|
||||||
|
<p>Share files and folders with anyone using secure links. Control access with permissions</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>High Performance</h3>
|
||||||
|
<p>Lightning-fast upload and download speeds powered by enterprise infrastructure</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||||
|
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||||
|
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>Auto Backup</h3>
|
||||||
|
<p>Never lose important files. Automatic backups keep multiple versions of your documents</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature-card">
|
||||||
|
<div class="feature-icon">
|
||||||
|
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="10"/>
|
||||||
|
<polyline points="12 6 12 12 16 14"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>24/7 Support</h3>
|
||||||
|
<p>Expert support team ready to help whenever you need. Email, chat, and phone support available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="use-cases-section">
|
||||||
|
<h2>Perfect for every need</h2>
|
||||||
|
<div class="use-cases-grid">
|
||||||
|
<div class="use-case-card">
|
||||||
|
<img src="/static/images/icon-families.svg" alt="Families Icon" class="use-case-icon">
|
||||||
<h3>For Families</h3>
|
<h3>For Families</h3>
|
||||||
<p>Securely backup and share precious photos and videos. Keep fond memories safe for generations.</p>
|
<p>Keep precious memories safe. Share photos and videos with family members securely.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="benefit-card professional-card">
|
<div class="use-case-card">
|
||||||
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="icon">
|
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="use-case-icon">
|
||||||
<h3>For Professionals</h3>
|
<h3>For Professionals</h3>
|
||||||
<p>Organize important work documents, collaborate in teams, and access files from anywhere.</p>
|
<p>Collaborate on projects, share documents, and work from anywhere with confidence.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="benefit-card student-card">
|
<div class="use-case-card">
|
||||||
<img src="/static/images/icon-students.svg" alt="Students Icon" class="icon">
|
<img src="/static/images/icon-students.svg" alt="Students Icon" class="use-case-icon">
|
||||||
<h3>For Students</h3>
|
<h3>For Students</h3>
|
||||||
<p>Store projects, notes, research papers. Access study materials across your devices.</p>
|
<p>Store all your coursework, projects, and study materials in one secure place.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a href="/solutions" class="btn-primary find-plan-btn">Find Your Perfect Plan</a>
|
</section>
|
||||||
|
|
||||||
|
<section class="cta-section">
|
||||||
|
<div class="cta-content">
|
||||||
|
<h2>Start storing your files securely today</h2>
|
||||||
|
<p>Join thousands of users who trust Retoor's Cloud Solutions</p>
|
||||||
|
<a href="/register" class="btn-primary cta-btn">Create Free Account</a>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
135
retoors/templates/pages/media_viewer.html
Normal file
135
retoors/templates/pages/media_viewer.html
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
{% extends "layouts/dashboard.html" %}
|
||||||
|
|
||||||
|
{% block title %}{{ file_path.split('/')[-1] }} - Viewer - Retoor's Cloud Solutions{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_head %}
|
||||||
|
<style>
|
||||||
|
.viewer-container {
|
||||||
|
height: calc(100vh - 200px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.viewer-toolbar {
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.viewer-toolbar .file-info {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.viewer-toolbar .actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.media-display {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
.media-display img, .media-display video, .media-display audio {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_title %}Viewing: {{ file_path }}{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_actions %}
|
||||||
|
<a href="/files?path={{ file_path.rsplit('/', 1)[0] }}" class="btn-outline">Back to Files</a>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_content %}
|
||||||
|
<div class="viewer-container">
|
||||||
|
<div class="viewer-toolbar">
|
||||||
|
<div class="file-info">
|
||||||
|
{{ file_path }}
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<!-- Add any actions if needed -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="media-display" id="media-display">
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const displayDiv = document.getElementById('media-display');
|
||||||
|
const filePath = '{{ file_path }}';
|
||||||
|
|
||||||
|
// Determine type based on file extension
|
||||||
|
const fileName = filePath.split('/').pop();
|
||||||
|
const ext = fileName.split('.').pop().toLowerCase();
|
||||||
|
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
|
||||||
|
const isVideo = ['mp4', 'webm', 'ogg', 'avi', 'mov'].includes(ext);
|
||||||
|
const isAudio = ['mp3', 'wav', 'ogg', 'aac'].includes(ext);
|
||||||
|
|
||||||
|
let mimeType = 'application/octet-stream';
|
||||||
|
if (isImage) {
|
||||||
|
const mimeMap = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', bmp: 'image/bmp', webp: 'image/webp', svg: 'image/svg+xml' };
|
||||||
|
mimeType = mimeMap[ext] || 'image/jpeg';
|
||||||
|
} else if (isVideo) {
|
||||||
|
const mimeMap = { mp4: 'video/mp4', webm: 'video/webm', ogg: 'video/ogg', avi: 'video/avi', mov: 'video/quicktime' };
|
||||||
|
mimeType = mimeMap[ext] || 'video/mp4';
|
||||||
|
} else if (isAudio) {
|
||||||
|
const mimeMap = { mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', aac: 'audio/aac' };
|
||||||
|
mimeType = mimeMap[ext] || 'audio/mpeg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isImage && !isVideo && !isAudio) {
|
||||||
|
displayDiv.innerHTML = '<p>Unsupported file type for viewing.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/file/content?path=${encodeURIComponent(filePath)}&binary=true`);
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.status === 'success') {
|
||||||
|
// Assume content is base64 encoded for binary files
|
||||||
|
const binaryString = atob(data.content);
|
||||||
|
const bytes = new Uint8Array(binaryString.length);
|
||||||
|
for (let i = 0; i < binaryString.length; i++) {
|
||||||
|
bytes[i] = binaryString.charCodeAt(i);
|
||||||
|
}
|
||||||
|
const blob = new Blob([bytes], { type: mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
if (isImage) {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = url;
|
||||||
|
img.onload = () => URL.revokeObjectURL(url);
|
||||||
|
displayDiv.innerHTML = '';
|
||||||
|
displayDiv.appendChild(img);
|
||||||
|
} else if (isVideo) {
|
||||||
|
const video = document.createElement('video');
|
||||||
|
video.src = url;
|
||||||
|
video.controls = true;
|
||||||
|
video.onload = () => URL.revokeObjectURL(url);
|
||||||
|
displayDiv.innerHTML = '';
|
||||||
|
displayDiv.appendChild(video);
|
||||||
|
} else if (isAudio) {
|
||||||
|
const audio = document.createElement('audio');
|
||||||
|
audio.src = url;
|
||||||
|
audio.controls = true;
|
||||||
|
audio.onload = () => URL.revokeObjectURL(url);
|
||||||
|
displayDiv.innerHTML = '';
|
||||||
|
displayDiv.appendChild(audio);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
displayDiv.innerHTML = '<p>Error loading file: ' + data.message + '</p>';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
displayDiv.innerHTML = '<p>Error loading file: ' + error.message + '</p>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
219
retoors/templates/pages/sla.html
Normal file
219
retoors/templates/pages/sla.html
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}Service Level Agreement{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<section class="content-section">
|
||||||
|
<h1>Service Level Agreement</h1>
|
||||||
|
<p>Last updated: January 2025</p>
|
||||||
|
<p>This Service Level Agreement applies to Business and Enterprise plan customers of Retoor's Cloud Solutions.</p>
|
||||||
|
|
||||||
|
<h2>1. Service Availability</h2>
|
||||||
|
|
||||||
|
<h3>1.1 Uptime Guarantee</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Service Tier</th>
|
||||||
|
<th>Monthly Uptime Guarantee</th>
|
||||||
|
<th>Maximum Downtime per Month</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Personal</td>
|
||||||
|
<td>99.0%</td>
|
||||||
|
<td>7.2 hours</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Professional</td>
|
||||||
|
<td>99.5%</td>
|
||||||
|
<td>3.6 hours</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Business</td>
|
||||||
|
<td>99.9%</td>
|
||||||
|
<td>43.2 minutes</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Enterprise</td>
|
||||||
|
<td>99.95%</td>
|
||||||
|
<td>21.6 minutes</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>1.2 Planned Maintenance</h3>
|
||||||
|
<p>Planned maintenance windows do not count against uptime guarantees. We will:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Provide at least 48 hours notice for planned maintenance</li>
|
||||||
|
<li>Schedule maintenance during off-peak hours when possible</li>
|
||||||
|
<li>Limit planned maintenance to 4 hours per month for Business plans</li>
|
||||||
|
<li>Limit planned maintenance to 2 hours per month for Enterprise plans</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>2. Support Response Times</h2>
|
||||||
|
|
||||||
|
<h3>2.1 Support Channels</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Service Tier</th>
|
||||||
|
<th>Support Channels</th>
|
||||||
|
<th>Support Hours</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Personal</td>
|
||||||
|
<td>Email only</td>
|
||||||
|
<td>Business hours (9-17 CET)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Professional</td>
|
||||||
|
<td>Email, Chat</td>
|
||||||
|
<td>Extended hours (8-20 CET)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Business</td>
|
||||||
|
<td>Email, Chat, Phone</td>
|
||||||
|
<td>24/7</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Enterprise</td>
|
||||||
|
<td>Email, Chat, Phone, Dedicated Account Manager</td>
|
||||||
|
<td>24/7</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>2.2 Response Time Commitments</h3>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Priority Level</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Business Plan</th>
|
||||||
|
<th>Enterprise Plan</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Critical (P1)</td>
|
||||||
|
<td>Service completely unavailable</td>
|
||||||
|
<td>1 hour</td>
|
||||||
|
<td>30 minutes</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>High (P2)</td>
|
||||||
|
<td>Major functionality impaired</td>
|
||||||
|
<td>4 hours</td>
|
||||||
|
<td>2 hours</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Medium (P3)</td>
|
||||||
|
<td>Minor functionality issues</td>
|
||||||
|
<td>1 business day</td>
|
||||||
|
<td>8 hours</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Low (P4)</td>
|
||||||
|
<td>General questions, feature requests</td>
|
||||||
|
<td>2 business days</td>
|
||||||
|
<td>1 business day</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2>3. Data Backup Guarantees</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Backup Frequency:</strong> Daily automated backups for all paid plans</li>
|
||||||
|
<li><strong>Retention Period:</strong> 30 days for Business plans, 90 days for Enterprise plans</li>
|
||||||
|
<li><strong>Recovery Point Objective (RPO):</strong> 24 hours maximum data loss</li>
|
||||||
|
<li><strong>Recovery Time Objective (RTO):</strong> 4 hours for Business, 2 hours for Enterprise</li>
|
||||||
|
<li><strong>Data Redundancy:</strong> All data stored with triple redundancy across multiple data centers</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>4. Performance Standards</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>File Upload Speed:</strong> Minimum 10 Mbps under normal conditions</li>
|
||||||
|
<li><strong>File Download Speed:</strong> Minimum 25 Mbps under normal conditions</li>
|
||||||
|
<li><strong>API Response Time:</strong> 95% of requests completed within 500ms</li>
|
||||||
|
<li><strong>File Access Latency:</strong> Maximum 100ms for file metadata operations</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>5. Service Credits</h2>
|
||||||
|
|
||||||
|
<h3>5.1 Credit Calculation</h3>
|
||||||
|
<p>If we fail to meet the uptime guarantee, you are eligible for service credits:</p>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Monthly Uptime Percentage</th>
|
||||||
|
<th>Service Credit</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>99.0% - 99.5%</td>
|
||||||
|
<td>10% of monthly fee</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>95.0% - 99.0%</td>
|
||||||
|
<td>25% of monthly fee</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>90.0% - 95.0%</td>
|
||||||
|
<td>50% of monthly fee</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Below 90.0%</td>
|
||||||
|
<td>100% of monthly fee</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>5.2 Claiming Credits</h3>
|
||||||
|
<p>To claim service credits:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Submit a claim within 30 days of the incident</li>
|
||||||
|
<li>Provide details of the downtime experienced</li>
|
||||||
|
<li>Credits will be applied to your next monthly invoice</li>
|
||||||
|
<li>Credits cannot be exchanged for cash</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>6. Exclusions</h2>
|
||||||
|
<p>This SLA does not apply to service unavailability caused by:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Factors outside our reasonable control (force majeure)</li>
|
||||||
|
<li>Your equipment, software, or internet connection</li>
|
||||||
|
<li>Violation of our Acceptable Use Policy</li>
|
||||||
|
<li>Scheduled maintenance with proper notice</li>
|
||||||
|
<li>Suspension or termination of your account for breach of terms</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>7. Monitoring and Reporting</h2>
|
||||||
|
<p>We provide:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Real-time service status dashboard at status.retoors.nl</li>
|
||||||
|
<li>Monthly uptime reports for Business and Enterprise customers</li>
|
||||||
|
<li>Email notifications for incidents affecting your service</li>
|
||||||
|
<li>Post-incident reports for P1 and P2 incidents</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>8. Changes to This SLA</h2>
|
||||||
|
<p>We may modify this SLA with 30 days notice. Material changes that reduce service levels will allow you to terminate your contract without penalty.</p>
|
||||||
|
|
||||||
|
<h2>9. Contact</h2>
|
||||||
|
<p>For SLA-related questions or to report service issues:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Email: sla@retoors.nl</li>
|
||||||
|
<li>Phone (Business/Enterprise): [Business Support Number]</li>
|
||||||
|
<li>Status Page: status.retoors.nl</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
88
retoors/templates/pages/user_rights.html
Normal file
88
retoors/templates/pages/user_rights.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{% extends "layouts/base.html" %}
|
||||||
|
{% block title %}User Rights Request{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="/static/css/components/content_pages.css">
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<main>
|
||||||
|
<section class="content-section">
|
||||||
|
<h1>User Rights Request</h1>
|
||||||
|
<p>Under the General Data Protection Regulation (GDPR), you have several rights regarding your personal data. You can exercise these rights by submitting a request below.</p>
|
||||||
|
|
||||||
|
<h2>Your Rights Under GDPR</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Right to Access:</strong> You can request a copy of all personal data we hold about you.</li>
|
||||||
|
<li><strong>Right to Rectification:</strong> You can request that we correct any inaccurate or incomplete personal data.</li>
|
||||||
|
<li><strong>Right to Erasure:</strong> You can request that we delete your personal data under certain circumstances.</li>
|
||||||
|
<li><strong>Right to Data Portability:</strong> You can request a copy of your data in a machine-readable format.</li>
|
||||||
|
<li><strong>Right to Object:</strong> You can object to certain types of processing of your personal data.</li>
|
||||||
|
<li><strong>Right to Restrict Processing:</strong> You can request that we limit the processing of your personal data.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Request Data Access</h2>
|
||||||
|
<p>Request a download of all your personal data that we store.</p>
|
||||||
|
<form action="/user_rights/access" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email">Your Email Address</label>
|
||||||
|
<input type="email" id="email" name="email" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="message">Additional Information (Optional)</label>
|
||||||
|
<textarea id="message" name="message" class="form-input" rows="4"></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Request Data Access</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Request Data Deletion</h2>
|
||||||
|
<p>Request the permanent deletion of your account and all associated data.</p>
|
||||||
|
<form action="/user_rights/delete" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email_delete">Your Email Address</label>
|
||||||
|
<input type="email" id="email_delete" name="email" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="reason">Reason for Deletion (Optional)</label>
|
||||||
|
<textarea id="reason" name="reason" class="form-input" rows="4"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="confirm" required>
|
||||||
|
I understand that this action is permanent and all my data will be deleted.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-danger">Request Data Deletion</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Request Data Correction</h2>
|
||||||
|
<p>Request correction of inaccurate or incomplete personal data.</p>
|
||||||
|
<form action="/user_rights/correct" method="post">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="email_correct">Your Email Address</label>
|
||||||
|
<input type="email" id="email_correct" name="email" class="form-input" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="correction_details">What needs to be corrected?</label>
|
||||||
|
<textarea id="correction_details" name="correction_details" class="form-input" rows="4" required></textarea>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Request Data Correction</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<h2>Response Time</h2>
|
||||||
|
<p>We will respond to your request within 30 days of receipt. If we need additional time, we will notify you and provide a reason for the delay.</p>
|
||||||
|
|
||||||
|
<h2>Contact for Privacy Concerns</h2>
|
||||||
|
<p>If you have any questions about your rights or how we process your data, please contact us at: <a href="mailto:privacy@retoors.nl">privacy@retoors.nl</a></p>
|
||||||
|
|
||||||
|
<h2>File a Complaint</h2>
|
||||||
|
<p>If you believe your data protection rights have been violated, you have the right to lodge a complaint with the Dutch Data Protection Authority (Autoriteit Persoonsgegevens):</p>
|
||||||
|
<p>
|
||||||
|
<strong>Autoriteit Persoonsgegevens</strong><br>
|
||||||
|
Postbus 93374<br>
|
||||||
|
2509 AJ Den Haag<br>
|
||||||
|
The Netherlands<br>
|
||||||
|
Website: <a href="https://autoriteitpersoonsgegevens.nl" target="_blank">https://autoriteitpersoonsgegevens.nl</a>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
84
retoors/views/editor.py
Normal file
84
retoors/views/editor.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
from aiohttp import web
|
||||||
|
import aiohttp_jinja2
|
||||||
|
from aiohttp.web_response import json_response
|
||||||
|
|
||||||
|
from ..helpers.auth import login_required
|
||||||
|
|
||||||
|
|
||||||
|
class FileEditorView(web.View):
|
||||||
|
@login_required
|
||||||
|
async def get(self):
|
||||||
|
file_path = self.request.query.get('path', '')
|
||||||
|
user = self.request.get('user')
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return web.Response(text="No file path specified", status=400)
|
||||||
|
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
'pages/file_editor.html',
|
||||||
|
self.request,
|
||||||
|
{
|
||||||
|
'request': self.request,
|
||||||
|
'user': user,
|
||||||
|
'file_path': file_path,
|
||||||
|
'active_page': 'files'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
async def post(self):
|
||||||
|
user_email = self.request['user']['email']
|
||||||
|
file_service = self.request.app['file_service']
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = await self.request.json()
|
||||||
|
file_path = data.get('path')
|
||||||
|
content = data.get('content')
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return json_response({'status': 'error', 'message': 'No file path specified'}, status=400)
|
||||||
|
|
||||||
|
if content is None:
|
||||||
|
return json_response({'status': 'error', 'message': 'No content provided'}, status=400)
|
||||||
|
|
||||||
|
success = await file_service.save_file_content(user_email, file_path, content)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return json_response({'status': 'success', 'message': 'File saved successfully'})
|
||||||
|
else:
|
||||||
|
return json_response({'status': 'error', 'message': 'Failed to save file'}, status=500)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({'status': 'error', 'message': str(e)}, status=500)
|
||||||
|
|
||||||
|
|
||||||
|
class FileContentView(web.View):
|
||||||
|
@login_required
|
||||||
|
async def get(self):
|
||||||
|
user_email = self.request['user']['email']
|
||||||
|
file_service = self.request.app['file_service']
|
||||||
|
file_path = self.request.query.get('path', '')
|
||||||
|
binary = self.request.query.get('binary', 'false').lower() == 'true'
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return json_response({'status': 'error', 'message': 'No file path specified'}, status=400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if binary:
|
||||||
|
content_bytes = await file_service.read_file_content_binary(user_email, file_path)
|
||||||
|
if content_bytes is not None:
|
||||||
|
import base64
|
||||||
|
content = base64.b64encode(content_bytes).decode('utf-8')
|
||||||
|
return json_response({'status': 'success', 'content': content})
|
||||||
|
else:
|
||||||
|
return json_response({'status': 'error', 'message': 'File not found or cannot be read'}, status=404)
|
||||||
|
else:
|
||||||
|
content = await file_service.read_file_content(user_email, file_path)
|
||||||
|
|
||||||
|
if content is not None:
|
||||||
|
return json_response({'status': 'success', 'content': content})
|
||||||
|
else:
|
||||||
|
return json_response({'status': 'error', 'message': 'File not found or cannot be read'}, status=404)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return json_response({'status': 'error', 'message': str(e)}, status=500)
|
||||||
@ -38,6 +38,18 @@ class SiteView(web.View):
|
|||||||
return await self.terms()
|
return await self.terms()
|
||||||
elif self.request.path == "/privacy":
|
elif self.request.path == "/privacy":
|
||||||
return await self.privacy()
|
return await self.privacy()
|
||||||
|
elif self.request.path == "/cookies":
|
||||||
|
return await self.cookies()
|
||||||
|
elif self.request.path == "/impressum":
|
||||||
|
return await self.impressum()
|
||||||
|
elif self.request.path == "/user_rights":
|
||||||
|
return await self.user_rights()
|
||||||
|
elif self.request.path == "/aup":
|
||||||
|
return await self.aup()
|
||||||
|
elif self.request.path == "/sla":
|
||||||
|
return await self.sla()
|
||||||
|
elif self.request.path == "/compliance":
|
||||||
|
return await self.compliance()
|
||||||
elif self.request.path == "/shared":
|
elif self.request.path == "/shared":
|
||||||
return await self.shared()
|
return await self.shared()
|
||||||
elif self.request.path == "/recent":
|
elif self.request.path == "/recent":
|
||||||
@ -92,6 +104,36 @@ 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")}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def cookies(self):
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/cookies.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def impressum(self):
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/impressum.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def user_rights(self):
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/user_rights.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def aup(self):
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/aup.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def sla(self):
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/sla.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
|
)
|
||||||
|
|
||||||
|
async def compliance(self):
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
"pages/compliance.html", self.request, {"request": self.request, "errors": {}, "user": self.request.get("user")}
|
||||||
|
)
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
async def shared(self):
|
async def shared(self):
|
||||||
return aiohttp_jinja2.render_template(
|
return aiohttp_jinja2.render_template(
|
||||||
@ -141,6 +183,15 @@ class FileBrowserView(web.View):
|
|||||||
path = self.request.query.get("path", "")
|
path = self.request.query.get("path", "")
|
||||||
files = await file_service.list_files(user_email, path)
|
files = await file_service.list_files(user_email, path)
|
||||||
|
|
||||||
|
# Determine editable and viewable files based on extension
|
||||||
|
editable_extensions = {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.xml', '.yaml', '.yml', '.ini', '.cfg', '.log', '.sh', '.bat', '.ps1', '.php', '.rb', '.java', '.c', '.cpp', '.h', '.hpp'}
|
||||||
|
viewable_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv', '.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a'}
|
||||||
|
for item in files:
|
||||||
|
if not item['is_dir']:
|
||||||
|
ext = Path(item['name']).suffix.lower()
|
||||||
|
item['is_editable'] = ext in editable_extensions
|
||||||
|
item['is_viewable'] = ext in viewable_extensions
|
||||||
|
|
||||||
success_message = self.request.query.get("success")
|
success_message = self.request.query.get("success")
|
||||||
error_message = self.request.query.get("error")
|
error_message = self.request.query.get("error")
|
||||||
|
|
||||||
|
|||||||
@ -9,30 +9,44 @@ class UploadView(web.View):
|
|||||||
async def post(self):
|
async def post(self):
|
||||||
user_email = self.request["user"]["email"]
|
user_email = self.request["user"]["email"]
|
||||||
file_service = self.request.app["file_service"]
|
file_service = self.request.app["file_service"]
|
||||||
# Get current path from query parameter or form data
|
current_path = ""
|
||||||
current_path = self.request.query.get("current_path", "")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
reader = await self.request.multipart()
|
reader = await self.request.multipart()
|
||||||
files_uploaded = []
|
files_uploaded = []
|
||||||
errors = []
|
errors = []
|
||||||
|
pending_files = []
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
field = await reader.next()
|
field = await reader.next()
|
||||||
if field is None:
|
if field is None:
|
||||||
break
|
break
|
||||||
|
|
||||||
# Check if the field is a file input
|
if field.name == "current_path":
|
||||||
if field.name == "file": # Assuming the input field name is 'file'
|
current_path = (await field.read()).decode('utf-8').strip()
|
||||||
|
print(f"Upload: current_path received: '{current_path}'")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if field.name == "file":
|
||||||
filename = field.filename
|
filename = field.filename
|
||||||
if not filename:
|
if not filename:
|
||||||
errors.append("Filename is required for one of the files.")
|
errors.append("Filename is required for one of the files.")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
content = await field.read()
|
content = await field.read()
|
||||||
# Construct the full file path relative to the user's base directory
|
pending_files.append((filename, content))
|
||||||
full_file_path_for_service = f"{current_path}/{filename}" if current_path else filename
|
|
||||||
|
|
||||||
|
print(f"Upload: Processing {len(pending_files)} files to path: '{current_path}'")
|
||||||
|
|
||||||
|
for filename, content in pending_files:
|
||||||
|
if current_path and not current_path.endswith('/'):
|
||||||
|
full_file_path_for_service = f"{current_path}/{filename}"
|
||||||
|
elif current_path:
|
||||||
|
full_file_path_for_service = f"{current_path}{filename}"
|
||||||
|
else:
|
||||||
|
full_file_path_for_service = filename
|
||||||
|
|
||||||
|
print(f"Upload: Uploading file to: {full_file_path_for_service}")
|
||||||
success = await file_service.upload_file(user_email, full_file_path_for_service, content)
|
success = await file_service.upload_file(user_email, full_file_path_for_service, content)
|
||||||
if success:
|
if success:
|
||||||
files_uploaded.append(filename)
|
files_uploaded.append(filename)
|
||||||
|
|||||||
44
retoors/views/viewer.py
Normal file
44
retoors/views/viewer.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from aiohttp import web
|
||||||
|
import aiohttp_jinja2
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..helpers.auth import login_required
|
||||||
|
|
||||||
|
|
||||||
|
class ViewerView(web.View):
|
||||||
|
@login_required
|
||||||
|
async def get(self):
|
||||||
|
file_path = self.request.query.get('path', '')
|
||||||
|
user = self.request.get('user')
|
||||||
|
|
||||||
|
if not file_path:
|
||||||
|
return web.Response(text="No file path specified", status=400)
|
||||||
|
|
||||||
|
# Get file extension to determine media type
|
||||||
|
file_ext = Path(file_path).suffix.lower()
|
||||||
|
|
||||||
|
# Determine media type
|
||||||
|
image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'}
|
||||||
|
video_extensions = {'.mp4', '.avi', '.mov', '.wmv', '.flv', '.webm', '.mkv'}
|
||||||
|
audio_extensions = {'.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a'}
|
||||||
|
|
||||||
|
if file_ext in image_extensions:
|
||||||
|
media_type = 'image'
|
||||||
|
elif file_ext in video_extensions:
|
||||||
|
media_type = 'video'
|
||||||
|
elif file_ext in audio_extensions:
|
||||||
|
media_type = 'audio'
|
||||||
|
else:
|
||||||
|
return web.Response(text="Unsupported file type for viewing", status=400)
|
||||||
|
|
||||||
|
return aiohttp_jinja2.render_template(
|
||||||
|
'pages/media_viewer.html',
|
||||||
|
self.request,
|
||||||
|
{
|
||||||
|
'request': self.request,
|
||||||
|
'user': user,
|
||||||
|
'file_path': file_path,
|
||||||
|
'media_type': media_type,
|
||||||
|
'active_page': 'files'
|
||||||
|
}
|
||||||
|
)
|
||||||
Loading…
Reference in New Issue
Block a user