Compare commits

..

No commits in common. "b374f7cd4f1e8e63b3b364d2320c3ed1f9bbd32f" and "cd259b0b81e87f075b9a9a1a5a9d765c7926e6af" have entirely different histories.

42 changed files with 1014 additions and 2799 deletions

View File

@ -1,4 +1,4 @@
import os
import argparse
from aiohttp import web
import aiohttp_jinja2
@ -73,8 +73,8 @@ def create_app():
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--port', type=int, default=os.getenv("PORT", 9001))
parser.add_argument('--hostname', default=os.getenv("HOSTNAME", "127.0.0.1"))
parser.add_argument('--port', type=int, default=8080)
parser.add_argument('--hostname', default='0.0.0.0')
args = parser.parse_args()
app = create_app()
web.run_app(app, host=args.hostname, port=args.port)

View File

@ -5,16 +5,6 @@ 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.editor import FileEditorView, FileContentView
from .views.viewer import ViewerView
from .views.sharing import (
create_share_page,
create_share_handler,
view_share,
download_shared_file,
manage_shares,
get_share_details,
update_share,
get_item_shares
)
def setup_routes(app):
app.router.add_view("/login", LoginView, name="login")
@ -23,6 +13,9 @@ def setup_routes(app):
app.router.add_view("/forgot_password", ForgotPasswordView, name="forgot_password")
app.router.add_view("/reset_password/{token}", ResetPasswordView, name="reset_password")
app.router.add_view("/", SiteView, name="index")
app.router.add_view("/solutions", SiteView, name="solutions")
app.router.add_view("/pricing", SiteView, name="pricing")
app.router.add_view("/security", SiteView, name="security")
app.router.add_view("/support", SiteView, name="support")
app.router.add_view("/use_cases", SiteView, name="use_cases")
app.router.add_view("/dashboard", SiteView, name="dashboard")
@ -66,12 +59,3 @@ def setup_routes(app):
app.router.add_delete("/api/users/{email}", delete_user, name="api_delete_user")
app.router.add_get("/api/users/{email}", get_user_details, name="api_get_user_details")
app.router.add_delete("/api/teams/{parent_email}", delete_team, name="api_delete_team")
app.router.add_get("/sharing/create", create_share_page, name="create_share_page")
app.router.add_post("/api/sharing/create", create_share_handler, name="create_share")
app.router.add_get("/share/{share_id}", view_share, name="view_share")
app.router.add_get("/share/{share_id}/download", download_shared_file, name="download_share")
app.router.add_get("/sharing/manage", manage_shares, name="manage_shares")
app.router.add_get("/api/sharing/{share_id}", get_share_details, name="get_share_details")
app.router.add_put("/api/sharing/{share_id}", update_share, name="update_share")
app.router.add_get("/api/sharing/item/shares", get_item_shares, name="get_item_shares")

View File

@ -8,7 +8,6 @@ import logging
import hashlib
import os
from .storage_service import StorageService
from .sharing_service import SharingService
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
@ -22,7 +21,6 @@ class FileService:
self.base_dir = base_dir
self.user_service = user_service
self.storage = StorageService()
self.sharing_service = SharingService()
self.base_dir.mkdir(parents=True, exist_ok=True)
self.drives_dir = self.base_dir / "drives"
self.drives_dir.mkdir(exist_ok=True)
@ -65,12 +63,6 @@ class FileService:
# Normalize path
if path and not path.endswith('/'):
path += '/'
# Update last_accessed for the folder if path is not root
if path:
folder_path = path.rstrip('/')
if folder_path in metadata and metadata[folder_path].get("type") == "dir":
metadata[folder_path]["last_accessed"] = datetime.datetime.now().isoformat()
await self._save_metadata(user_email, metadata)
items = []
seen = set()
for item_path, item_meta in metadata.items():
@ -99,7 +91,6 @@ class FileService:
"type": "dir",
"created_at": datetime.datetime.now().isoformat(),
"modified_at": datetime.datetime.now().isoformat(),
"last_accessed": datetime.datetime.now().isoformat(),
}
await self._save_metadata(user_email, metadata)
logger.info(f"create_folder: Folder created: {folder_path}")
@ -124,7 +115,6 @@ class FileService:
"blob_location": {"drive": drive, "path": f"{dir1}/{dir2}/{dir3}/{hash}"},
"created_at": datetime.datetime.now().isoformat(),
"modified_at": datetime.datetime.now().isoformat(),
"last_accessed": datetime.datetime.now().isoformat(),
}
await self._save_metadata(user_email, metadata)
logger.info(f"upload_file: File uploaded to drive {drive}: {file_path}")
@ -137,9 +127,6 @@ class FileService:
logger.warning(f"download_file: File not found in metadata: {file_path}")
return None
item_meta = metadata[file_path]
# Update last_accessed
item_meta["last_accessed"] = datetime.datetime.now().isoformat()
await self._save_metadata(user_email, metadata)
blob_loc = item_meta["blob_location"]
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
if not blob_path.exists():
@ -157,9 +144,6 @@ class FileService:
logger.warning(f"read_file_content: File not found in metadata: {file_path}")
return None
item_meta = metadata[file_path]
# Update last_accessed
item_meta["last_accessed"] = datetime.datetime.now().isoformat()
await self._save_metadata(user_email, metadata)
blob_loc = item_meta["blob_location"]
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
if not blob_path.exists():
@ -184,9 +168,6 @@ class FileService:
logger.warning(f"read_file_content: File not found in metadata: {file_path}")
return None
item_meta = metadata[file_path]
# Update last_accessed
item_meta["last_accessed"] = datetime.datetime.now().isoformat()
await self._save_metadata(user_email, metadata)
blob_loc = item_meta["blob_location"]
blob_path = self.drives_dir / blob_loc["drive"] / blob_loc["path"]
if not blob_path.exists():
@ -227,92 +208,77 @@ class FileService:
logger.info(f"delete_item: Item deleted: {item_path}")
return True
async def generate_share_link(
self,
user_email: str,
item_path: str,
permission: str = "view",
scope: str = "public",
password: str = None,
expiration_days: int = None,
disable_download: bool = False,
recipient_emails: list = None
) -> str | None:
async def generate_share_link(self, user_email: str, item_path: str) -> str | None:
"""Generates a shareable link for a file or folder."""
logger.debug(f"generate_share_link: Generating link for user '{user_email}', item '{item_path}'")
metadata = await self._load_metadata(user_email)
if item_path not in metadata:
logger.warning(f"generate_share_link: Item does not exist: {item_path}")
return None
user = await self.user_service.get_user_by_email(user_email)
if not user:
logger.warning(f"generate_share_link: User not found: {user_email}")
return None
share_id = await self.sharing_service.create_share(
owner_email=user_email,
item_path=item_path,
permission=permission,
scope=scope,
password=password,
expiration_days=expiration_days,
disable_download=disable_download,
recipient_emails=recipient_emails
)
share_id = str(uuid.uuid4())
if "shared_items" not in user:
user["shared_items"] = {}
user["shared_items"][share_id] = {
"user_email": user_email,
"item_path": item_path,
"created_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"expires_at": (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=7)).isoformat(), # 7-day expiry
}
await self.user_service.update_user(user_email, shared_items=user["shared_items"])
logger.info(f"generate_share_link: Share link generated with ID: {share_id} for item: {item_path}")
return share_id
async def get_shared_item(self, share_id: str, password: str = None, accessor_email: str = None) -> dict | None:
async def get_shared_item(self, share_id: str) -> dict | None:
"""Retrieves information about a shared item."""
logger.debug(f"get_shared_item: Retrieving shared item with ID: {share_id}")
all_users = await self.user_service.get_all_users()
for user in all_users:
if "shared_items" in user and share_id in user["shared_items"]:
shared_item = user["shared_items"][share_id]
expiry_time = datetime.datetime.fromisoformat(shared_item["expires_at"])
if expiry_time > datetime.datetime.now(datetime.timezone.utc):
logger.info(f"get_shared_item: Found valid shared item for ID: {share_id}")
return shared_item
else:
logger.warning(f"get_shared_item: Shared item {share_id} has expired.")
logger.warning(f"get_shared_item: No valid shared item found for ID: {share_id}")
return None
share = await self.sharing_service.get_share(share_id)
if not share:
logger.warning(f"get_shared_item: No valid shared item found for ID: {share_id}")
return None
if not await self.sharing_service.verify_share_access(share_id, password, accessor_email):
logger.warning(f"get_shared_item: Access denied for share {share_id}")
return None
await self.sharing_service.record_share_access(share_id, accessor_email)
logger.info(f"get_shared_item: Found valid shared item for ID: {share_id}")
return share
async def get_shared_file_content(self, share_id: str, password: str = None, accessor_email: str = None, requested_file_path: str = None) -> tuple[bytes, str] | None:
async def get_shared_file_content(self, share_id: str, requested_file_path: str | None = None) -> tuple[bytes, str] | None:
"""Retrieves the content of a shared file."""
logger.debug(f"get_shared_file_content: Retrieving content for shared file with ID: {share_id}, requested_file_path: {requested_file_path}")
shared_item = await self.get_shared_item(share_id, password, accessor_email)
shared_item = await self.get_shared_item(share_id)
if not shared_item:
return None
if shared_item.get("disable_download", False):
logger.warning(f"get_shared_file_content: Download disabled for share {share_id}")
return None
user_email = shared_item["owner_email"]
item_path = shared_item["item_path"]
user_email = shared_item["user_email"]
item_path = shared_item["item_path"] # This is the path of the originally shared item (file or folder)
target_path = item_path
if requested_file_path:
target_path = requested_file_path
if not target_path.startswith(item_path + '/') and target_path != item_path:
# Security check: Ensure the requested file is actually within the shared item's directory
if not target_path.startswith(item_path + '/'):
logger.warning(f"get_shared_file_content: Requested file path '{requested_file_path}' is not within shared item path '{item_path}' for share_id: {share_id}")
return None
return await self.download_file(user_email, target_path)
async def get_shared_folder_content(self, share_id: str, password: str = None, accessor_email: str = None) -> list | None:
async def get_shared_folder_content(self, share_id: str) -> list | None:
"""Retrieves the content of a shared folder."""
logger.debug(f"get_shared_folder_content: Retrieving content for shared folder with ID: {share_id}")
shared_item = await self.get_shared_item(share_id, password, accessor_email)
shared_item = await self.get_shared_item(share_id)
if not shared_item:
return None
user_email = shared_item["owner_email"]
user_email = shared_item["user_email"]
item_path = shared_item["item_path"]
metadata = await self._load_metadata(user_email)
if item_path not in metadata or metadata[item_path]["type"] != "dir":
logger.warning(f"get_shared_folder_content: Shared item is not a directory: {item_path}")
return None
@ -337,7 +303,6 @@ class FileService:
"type": "dir",
"created_at": datetime.datetime.now().isoformat(),
"modified_at": datetime.datetime.now().isoformat(),
"last_accessed": datetime.datetime.now().isoformat(),
}
migrated_count += 1
for file_name in files:
@ -365,28 +330,9 @@ class FileService:
"blob_location": {"drive": drive, "path": f"{dir1}/{dir2}/{dir3}/{hash}"},
"created_at": datetime.datetime.fromtimestamp(full_file_path.stat().st_ctime).isoformat(),
"modified_at": datetime.datetime.fromtimestamp(full_file_path.stat().st_mtime).isoformat(),
"last_accessed": datetime.datetime.fromtimestamp(full_file_path.stat().st_atime).isoformat(),
}
migrated_count += 1
await self._save_metadata(user_email, metadata)
logger.info(f"Migrated {migrated_count} items for {user_email}")
# Optionally remove old dir
# shutil.rmtree(old_user_dir)
async def get_recent_files(self, user_email: str, limit: int = 50) -> list:
"""Gets the most recently accessed files and folders for the user."""
metadata = await self._load_metadata(user_email)
items = []
for path, meta in metadata.items():
if meta.get("type") in ("file", "dir"):
last_accessed = meta.get("last_accessed", meta.get("modified_at", ""))
items.append({
"path": path,
"name": Path(path).name,
"is_dir": meta["type"] == "dir",
"size": meta.get("size", 0) if meta["type"] == "file" else 0,
"last_accessed": last_accessed,
})
# Sort by last_accessed descending
items.sort(key=lambda x: x["last_accessed"], reverse=True)
return items[:limit]

View File

@ -1,317 +0,0 @@
import uuid
import datetime
import hashlib
import logging
from typing import Dict, List, Optional, Any
from .storage_service import StorageService
logger = logging.getLogger(__name__)
class SharingService:
PERMISSION_VIEW = "view"
PERMISSION_EDIT = "edit"
PERMISSION_COMMENT = "comment"
SCOPE_PUBLIC = "public"
SCOPE_PRIVATE = "private"
SCOPE_ACCOUNT_BASED = "account_based"
def __init__(self):
self.storage = StorageService()
async def _load_shares(self, user_email: str) -> Dict:
shares = await self.storage.load(user_email, "shares")
return shares if shares else {}
async def _save_shares(self, user_email: str, shares: Dict):
await self.storage.save(user_email, "shares", shares)
async def _load_share_recipients(self, share_id: str) -> Dict:
recipients = await self.storage.load("global_shares", f"recipients_{share_id}")
return recipients if recipients else {}
async def _save_share_recipients(self, share_id: str, recipients: Dict):
await self.storage.save("global_shares", f"recipients_{share_id}", recipients)
async def _load_all_shares(self) -> Dict:
all_shares = await self.storage.load("global_shares", "all_shares_index")
return all_shares if all_shares else {}
async def _save_all_shares(self, all_shares: Dict):
await self.storage.save("global_shares", "all_shares_index", all_shares)
def _hash_password(self, password: str) -> str:
return hashlib.sha256(password.encode()).hexdigest()
async def create_share(
self,
owner_email: str,
item_path: str,
permission: str = PERMISSION_VIEW,
scope: str = SCOPE_PUBLIC,
password: Optional[str] = None,
expiration_days: Optional[int] = None,
disable_download: bool = False,
recipient_emails: Optional[List[str]] = None
) -> str:
share_id = str(uuid.uuid4())
now = datetime.datetime.now(datetime.timezone.utc)
expires_at = None
if expiration_days:
expires_at = (now + datetime.timedelta(days=expiration_days)).isoformat()
password_hash = None
if password:
password_hash = self._hash_password(password)
share_data = {
"share_id": share_id,
"owner_email": owner_email,
"item_path": item_path,
"permission": permission,
"scope": scope,
"password_hash": password_hash,
"created_at": now.isoformat(),
"expires_at": expires_at,
"disable_download": disable_download,
"active": True,
"access_count": 0,
"last_accessed": None
}
user_shares = await self._load_shares(owner_email)
user_shares[share_id] = share_data
await self._save_shares(owner_email, user_shares)
all_shares = await self._load_all_shares()
all_shares[share_id] = {
"owner_email": owner_email,
"item_path": item_path,
"created_at": now.isoformat()
}
await self._save_all_shares(all_shares)
if recipient_emails and scope in [self.SCOPE_PRIVATE, self.SCOPE_ACCOUNT_BASED]:
recipients = {}
for email in recipient_emails:
recipients[email] = {
"email": email,
"permission": permission,
"invited_at": now.isoformat(),
"accessed": False
}
await self._save_share_recipients(share_id, recipients)
logger.info(f"Created share {share_id} for {item_path} by {owner_email}")
return share_id
async def get_share(self, share_id: str) -> Optional[Dict]:
all_shares = await self._load_all_shares()
if share_id not in all_shares:
return None
owner_email = all_shares[share_id]["owner_email"]
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return None
share = user_shares[share_id]
if not share.get("active", True):
logger.warning(f"Share {share_id} is deactivated")
return None
if share.get("expires_at"):
expiry_time = datetime.datetime.fromisoformat(share["expires_at"])
if expiry_time <= datetime.datetime.now(datetime.timezone.utc):
logger.warning(f"Share {share_id} has expired")
return None
return share
async def verify_share_access(
self,
share_id: str,
password: Optional[str] = None,
accessor_email: Optional[str] = None
) -> bool:
share = await self.get_share(share_id)
if not share:
return False
if share.get("password_hash") and password:
if self._hash_password(password) != share["password_hash"]:
logger.warning(f"Invalid password for share {share_id}")
return False
elif share.get("password_hash") and not password:
return False
if share["scope"] == self.SCOPE_PRIVATE and accessor_email:
recipients = await self._load_share_recipients(share_id)
if accessor_email not in recipients:
logger.warning(f"Email {accessor_email} not in recipients for share {share_id}")
return False
if share["scope"] == self.SCOPE_ACCOUNT_BASED and not accessor_email:
logger.warning(f"Account-based share {share_id} requires authenticated user")
return False
return True
async def record_share_access(self, share_id: str, accessor_email: Optional[str] = None):
all_shares = await self._load_all_shares()
if share_id not in all_shares:
return
owner_email = all_shares[share_id]["owner_email"]
user_shares = await self._load_shares(owner_email)
if share_id in user_shares:
user_shares[share_id]["access_count"] = user_shares[share_id].get("access_count", 0) + 1
user_shares[share_id]["last_accessed"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
await self._save_shares(owner_email, user_shares)
if accessor_email:
recipients = await self._load_share_recipients(share_id)
if accessor_email in recipients:
recipients[accessor_email]["accessed"] = True
recipients[accessor_email]["last_accessed"] = datetime.datetime.now(datetime.timezone.utc).isoformat()
await self._save_share_recipients(share_id, recipients)
async def deactivate_share(self, owner_email: str, share_id: str) -> bool:
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return False
user_shares[share_id]["active"] = False
await self._save_shares(owner_email, user_shares)
logger.info(f"Deactivated share {share_id}")
return True
async def reactivate_share(self, owner_email: str, share_id: str) -> bool:
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return False
user_shares[share_id]["active"] = True
await self._save_shares(owner_email, user_shares)
logger.info(f"Reactivated share {share_id}")
return True
async def delete_share(self, owner_email: str, share_id: str) -> bool:
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return False
del user_shares[share_id]
await self._save_shares(owner_email, user_shares)
all_shares = await self._load_all_shares()
if share_id in all_shares:
del all_shares[share_id]
await self._save_all_shares(all_shares)
logger.info(f"Deleted share {share_id}")
return True
async def update_share_permission(self, owner_email: str, share_id: str, permission: str) -> bool:
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return False
user_shares[share_id]["permission"] = permission
await self._save_shares(owner_email, user_shares)
logger.info(f"Updated permission for share {share_id} to {permission}")
return True
async def update_share_expiration(self, owner_email: str, share_id: str, expiration_days: Optional[int]) -> bool:
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return False
if expiration_days:
expires_at = (datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(days=expiration_days)).isoformat()
user_shares[share_id]["expires_at"] = expires_at
else:
user_shares[share_id]["expires_at"] = None
await self._save_shares(owner_email, user_shares)
logger.info(f"Updated expiration for share {share_id}")
return True
async def add_share_recipient(self, owner_email: str, share_id: str, recipient_email: str, permission: str) -> bool:
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return False
recipients = await self._load_share_recipients(share_id)
recipients[recipient_email] = {
"email": recipient_email,
"permission": permission,
"invited_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"accessed": False
}
await self._save_share_recipients(share_id, recipients)
logger.info(f"Added recipient {recipient_email} to share {share_id}")
return True
async def remove_share_recipient(self, owner_email: str, share_id: str, recipient_email: str) -> bool:
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return False
recipients = await self._load_share_recipients(share_id)
if recipient_email in recipients:
del recipients[recipient_email]
await self._save_share_recipients(share_id, recipients)
logger.info(f"Removed recipient {recipient_email} from share {share_id}")
return True
return False
async def update_recipient_permission(self, owner_email: str, share_id: str, recipient_email: str, permission: str) -> bool:
user_shares = await self._load_shares(owner_email)
if share_id not in user_shares:
return False
recipients = await self._load_share_recipients(share_id)
if recipient_email in recipients:
recipients[recipient_email]["permission"] = permission
await self._save_share_recipients(share_id, recipients)
logger.info(f"Updated permission for {recipient_email} in share {share_id} to {permission}")
return True
return False
async def get_share_recipients(self, share_id: str) -> Dict:
return await self._load_share_recipients(share_id)
async def list_user_shares(self, user_email: str) -> List[Dict]:
user_shares = await self._load_shares(user_email)
return list(user_shares.values())
async def get_shares_for_item(self, user_email: str, item_path: str) -> List[Dict]:
user_shares = await self._load_shares(user_email)
item_shares = [
share for share in user_shares.values()
if share["item_path"] == item_path
]
return item_shares

View File

@ -211,48 +211,3 @@ class UserService:
await self._storage_manager.save_user(email, user)
else:
await self._save_users()
async def add_favorite(self, email: str, file_path: str) -> bool:
user = await self.get_user_by_email(email)
if not user:
return False
if "favorites" not in user:
user["favorites"] = []
if file_path not in user["favorites"]:
user["favorites"].append(file_path)
if self.use_isolated_storage:
await self._storage_manager.save_user(email, user)
else:
await self._save_users()
return True
async def remove_favorite(self, email: str, file_path: str) -> bool:
user = await self.get_user_by_email(email)
if not user:
return False
if "favorites" in user and file_path in user["favorites"]:
user["favorites"].remove(file_path)
if self.use_isolated_storage:
await self._storage_manager.save_user(email, user)
else:
await self._save_users()
return True
async def get_favorites(self, email: str) -> List[str]:
user = await self.get_user_by_email(email)
if not user:
return []
return user.get("favorites", [])
async def is_favorite(self, email: str, file_path: str) -> bool:
user = await self.get_user_by_email(email)
if not user:
return False
return file_path in user.get("favorites", [])

View File

@ -1,32 +1,24 @@
:root {
--dutch-red: #AE1C28; /* Dutch flag red */
--dutch-white: #FFFFFF; /* Dutch flag white */
--dutch-blue: #21468B; /* Dutch flag blue */
--dutch-red-dark: #8B1621; /* Darker red for hover */
--dutch-blue-dark: #1A3766; /* Darker blue for hover */
--primary-color: var(--dutch-blue);
--accent-color: var(--dutch-red);
--red-accent: var(--dutch-red);
--red-hover: var(--dutch-red-dark);
--secondary-color: #F7FAFC;
--background-color: var(--dutch-white);
--text-color: #1A202C;
--light-text-color: #4A5568;
--border-color: #E2E8F0;
--card-background: var(--dutch-white);
--shadow-color: rgba(0, 0, 0, 0.05);
--primary-color: #0066FF; /* Vibrant blue */
--accent-color: #00D4FF; /* Bright cyan accent */
--secondary-color: #F7FAFC; /* Very light blue-grey */
--background-color: #FFFFFF; /* Pure white background */
--text-color: #1A202C; /* Dark blue-grey */
--light-text-color: #718096; /* Medium grey for descriptions */
--border-color: #E2E8F0; /* Light grey border */
--card-background: #FFFFFF; /* White for cards */
--shadow-color: rgba(0, 0, 0, 0.05); /* Very subtle shadow */
/* Button specific variables */
--btn-primary-bg: var(--dutch-blue);
--btn-primary-text: var(--dutch-white);
--btn-primary-hover-bg: var(--dutch-blue-dark);
--btn-secondary-bg: var(--dutch-red);
--btn-secondary-text: var(--dutch-white);
--btn-secondary-hover-bg: var(--dutch-red-dark);
--btn-outline-border: var(--dutch-blue);
--btn-outline-text: var(--dutch-blue);
--btn-outline-hover-bg: rgba(33, 70, 139, 0.05);
--btn-primary-bg: var(--primary-color);
--btn-primary-text: #FFFFFF;
--btn-primary-hover-bg: #0052CC; /* Darker blue */
--btn-secondary-bg: #EDF2F7; /* Light grey */
--btn-secondary-text: var(--text-color);
--btn-secondary-hover-bg: #E2E8F0; /* Darker grey */
--btn-outline-border: var(--primary-color);
--btn-outline-text: var(--primary-color);
--btn-outline-hover-bg: rgba(0, 102, 255, 0.05); /* Very light blue hover */
}
html, body {
@ -45,7 +37,7 @@ html, body {
/* General typography */
h1 {
font-size: clamp(1.75rem, 5vw, 3.5rem);
font-size: 3.5rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: var(--text-color);
@ -54,7 +46,7 @@ h1 {
}
h2 {
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--text-color);
@ -63,7 +55,7 @@ h2 {
}
h3 {
font-size: clamp(1.125rem, 3vw, 1.5rem);
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0.75rem;
color: var(--text-color);
@ -73,30 +65,30 @@ h3 {
p {
margin-bottom: 1rem;
color: var(--light-text-color);
font-size: clamp(0.875rem, 2vw, 1.125rem);
font-size: 1.125rem;
line-height: 1.7;
}
/* Card-like styling for sections */
.card {
background-color: var(--card-background);
border-radius: 12px;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
padding: clamp(1rem, 4vw, 3rem);
margin-bottom: clamp(20px, 3vw, 35px);
border-radius: 12px; /* Slightly more rounded corners */
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); /* Softer, more prominent shadow */
padding: 3rem; /* Increased padding */
margin-bottom: 35px; /* Increased margin */
}
.container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-start;
flex-direction: column; /* Allow content to stack vertically */
justify-content: flex-start; /* Align content to the top */
align-items: center;
padding: clamp(10px, 2vw, 20px);
padding: 20px;
box-sizing: border-box;
width: 100%;
max-width: 1200px;
margin: 0 auto;
width: 100%; /* Ensure container takes full width */
max-width: 1200px; /* Max width for overall content */
margin: 0 auto; /* Center the container */
}
.retoors-container {
@ -133,8 +125,8 @@ p {
}
.search-input:focus {
box-shadow: 0 1px 6px rgba(174, 28, 40, 0.25);
border-color: var(--dutch-red);
box-shadow: 0 1px 6px rgba(32,33,36,.28);
border-color: var(--accent-color);
}
.search-buttons {
@ -144,10 +136,10 @@ p {
}
.retoors-button {
background-color: #F0F0F0;
border: 1px solid #D0D0D0;
background-color: var(--btn-secondary-bg);
border: 1px solid var(--btn-secondary-bg);
border-radius: 4px;
color: var(--text-color);
color: var(--btn-secondary-text);
font-family: 'Roboto', sans-serif;
font-size: 15px;
padding: 10px 20px;
@ -158,23 +150,18 @@ p {
}
.retoors-button:hover {
background-color: #E0E0E0;
border-color: #B0B0B0;
background-color: var(--btn-secondary-hover-bg);
border-color: var(--btn-secondary-hover-bg);
}
/* Header and Navigation */
.site-header {
background: linear-gradient(to bottom, var(--dutch-red) 0%, var(--dutch-red) 33.33%, var(--dutch-white) 33.33%, var(--dutch-white) 66.66%, var(--dutch-blue) 66.66%, var(--dutch-blue) 100%);
background-size: 100% 6px;
background-repeat: no-repeat;
background-position: top;
background-color: var(--dutch-white);
background-color: #FFFFFF;
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 1000;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
padding-top: 6px;
}
.site-nav {
@ -230,7 +217,7 @@ p {
}
.nav-menu a:hover {
color: var(--dutch-red);
color: var(--primary-color);
}
.nav-actions {
@ -251,7 +238,7 @@ p {
}
.nav-link:hover {
color: var(--dutch-red);
color: var(--primary-color);
}
/* Main content area */
@ -357,7 +344,7 @@ main {
background-color: var(--btn-primary-hover-bg);
border-color: var(--btn-primary-hover-bg);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(33, 70, 139, 0.3);
box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
}
.btn-outline {
@ -379,27 +366,14 @@ main {
}
.btn-danger {
background-color: var(--red-accent);
background-color: #dc3545;
color: white;
border-color: var(--red-accent);
border-color: #dc3545;
}
.btn-danger:hover {
background-color: var(--red-hover);
border-color: var(--red-hover);
}
.btn-accent {
background-color: var(--dutch-red);
color: var(--dutch-white);
border-color: var(--dutch-red);
}
.btn-accent:hover {
background-color: var(--dutch-red-dark);
border-color: var(--dutch-red-dark);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(174, 28, 40, 0.3);
background-color: #c82333;
border-color: #bd2130;
}
.error {
@ -468,15 +442,15 @@ main {
@media (max-width: 480px) {
.brand-text {
font-size: 1.125rem;
font-size: 1.25rem;
}
.nav-menu {
gap: 0.5rem;
gap: 0.75rem;
}
.nav-menu a {
font-size: 0.8rem;
font-size: 0.85rem;
}
.nav-actions {
@ -490,67 +464,5 @@ main {
width: 100%;
max-width: 280px;
text-align: center;
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
.form-container {
padding: 1.5rem;
margin: 20px auto;
}
.form-container h2 {
font-size: 1.5rem;
}
.form-group input[type="email"],
.form-group input[type="password"],
.form-group input[type="text"],
.form-group input[type="number"] {
padding: 12px 14px;
}
.btn-primary,
.btn-outline {
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
}
@media (max-width: 360px) {
html, body {
font-size: 14px;
}
.site-nav {
padding: 8px 10px;
}
.brand-text {
font-size: 1rem;
}
.nav-menu a {
font-size: 0.75rem;
}
.card {
padding: 1rem;
margin-bottom: 15px;
}
.container {
padding: 8px;
}
.form-container {
padding: 1rem;
margin: 15px auto;
}
.btn-primary,
.btn-outline {
padding: 0.45rem 0.85rem;
font-size: 0.8rem;
}
}

View File

@ -20,8 +20,6 @@
max-height: calc(100vh - 120px);
overflow-y: hidden;
overflow-x: hidden;
border-top: 3px solid var(--dutch-red);
border-bottom: 3px solid var(--dutch-blue);
}
.dashboard-sidebar::-webkit-scrollbar {
@ -55,9 +53,8 @@
.sidebar-menu ul li a:hover,
.sidebar-menu ul li a.active {
background-color: var(--dutch-blue);
color: var(--dutch-white);
border-left: 3px solid var(--dutch-red);
background-color: var(--accent-color);
color: white;
}
.sidebar-menu ul li a img.icon {
@ -167,8 +164,8 @@
}
.file-search-bar:focus {
border-color: var(--dutch-red);
box-shadow: 0 0 0 2px rgba(174, 28, 40, 0.2);
border-color: var(--accent-color);
box-shadow: 0 0 0 2px rgba(0, 188, 212, 0.2);
}
.file-list-table {
@ -300,54 +297,11 @@
.file-list-table th,
.file-list-table td {
padding: 8px 10px;
padding: 10px 12px;
font-size: 0.85rem;
}
.file-list-table td {
max-width: 150px;
}
}
@media (max-width: 360px) {
.dashboard-layout {
padding: 8px;
}
.dashboard-sidebar {
padding: 12px;
}
.dashboard-content {
padding: 12px;
}
.dashboard-content-header h2 {
font-size: 1.25rem;
}
.dashboard-actions .btn-primary,
.dashboard-actions .btn-outline {
padding: 0.5rem 0.8rem;
font-size: 0.8rem;
}
.file-list-table {
font-size: 0.75rem;
}
.file-list-table th,
.file-list-table td {
padding: 6px 8px;
font-size: 0.75rem;
}
.file-list-table td {
max-width: 120px;
}
.file-search-bar {
padding: 8px 12px;
font-size: 0.9rem;
}
}

View File

@ -2,9 +2,7 @@ footer {
background-color: var(--card-background);
color: var(--light-text-color);
padding: 2rem 2rem 1rem 2rem;
border-top: 6px solid transparent;
border-image: linear-gradient(to right, var(--dutch-red) 0%, var(--dutch-red) 33.33%, var(--dutch-white) 33.33%, var(--dutch-white) 66.66%, var(--dutch-blue) 66.66%, var(--dutch-blue) 100%);
border-image-slice: 1;
border-top: 1px solid var(--border-color);
margin-top: auto;
box-shadow: 0 -2px 4px var(--shadow-color);
}
@ -46,7 +44,7 @@ footer {
}
.footer-section ul li a:hover {
color: var(--dutch-red);
color: var(--accent-color);
}
.footer-bottom {

View File

@ -1,106 +1 @@
/* Styles for Form Pages */
.form-page-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: clamp(30px, 5vh, 60px) clamp(15px, 3vw, 20px) clamp(40px, 6vh, 80px);
box-sizing: border-box;
width: 100%;
max-width: 100%;
}
.form-page-container h1 {
font-size: clamp(1.75rem, 4vw, 2.5rem);
color: var(--text-color);
margin-bottom: 10px;
text-align: center;
}
.form-page-container .subtitle {
font-size: clamp(0.95rem, 2vw, 1.1rem);
color: var(--light-text-color);
margin-bottom: clamp(20px, 4vh, 30px);
text-align: center;
max-width: 600px;
}
.form-page-container .form-container {
max-width: 450px;
width: 100%;
padding: clamp(1.5rem, 4vw, 2.5rem);
box-sizing: border-box;
margin-bottom: clamp(20px, 4vh, 30px);
}
.login-link {
display: block;
text-align: center;
margin-top: 25px;
font-size: 0.95rem;
color: var(--light-text-color);
}
.login-link a {
color: var(--primary-color);
font-weight: 500;
}
.login-link a:hover {
color: var(--dutch-red);
text-decoration: underline;
}
.message {
color: #2E7D32;
background-color: #E8F5E9;
border: 1px solid #81C784;
padding: 1rem 1.5rem;
margin-bottom: 1.5rem;
border-radius: 8px;
font-size: 0.95rem;
font-weight: 500;
text-align: center;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.form-page-container {
padding: 30px 15px 40px;
}
.form-page-container h1 {
font-size: 1.75rem;
}
.form-page-container .subtitle {
font-size: 0.95rem;
margin-bottom: 20px;
}
.form-page-container .form-container {
padding: 1.5rem;
}
}
@media (max-width: 360px) {
.form-page-container {
padding: 20px 10px 30px;
}
.form-page-container h1 {
font-size: 1.5rem;
margin-bottom: 8px;
}
.form-page-container .subtitle {
font-size: 0.875rem;
margin-bottom: 15px;
}
.form-page-container .form-container {
padding: 1rem;
}
.login-link {
font-size: 0.85rem;
}
.message {
font-size: 0.85rem;
padding: 0.75rem 1rem;
}
}

View File

@ -1,12 +1,9 @@
/* Hero Section */
.hero-section {
text-align: center;
padding: clamp(30px, 8vw, 80px) clamp(10px, 3vw, 20px) clamp(20px, 6vw, 60px);
background: linear-gradient(135deg, rgba(33, 70, 139, 0.03) 0%, var(--dutch-white) 100%);
padding: 100px 20px 80px;
background: linear-gradient(135deg, #F7FAFC 0%, #FFFFFF 100%);
margin-bottom: 0;
border-top: 4px solid transparent;
border-image: linear-gradient(to right, var(--dutch-red) 0%, var(--dutch-red) 33.33%, var(--dutch-white) 33.33%, var(--dutch-white) 66.66%, var(--dutch-blue) 66.66%, var(--dutch-blue) 100%);
border-image-slice: 1;
}
.hero-content {
@ -15,19 +12,19 @@
}
.hero-section h1 {
font-size: 3rem;
font-size: 3.75rem;
font-weight: 700;
color: var(--text-color);
margin-bottom: 1rem;
margin-bottom: 1.5rem;
line-height: 1.15;
letter-spacing: -0.03em;
}
.hero-subtitle {
font-size: 1.125rem;
font-size: 1.25rem;
color: var(--light-text-color);
max-width: 600px;
margin: 0 auto;
max-width: 700px;
margin: 0 auto 2.5rem;
line-height: 1.6;
}
@ -55,7 +52,7 @@
/* Features Section */
.features-section {
padding: clamp(30px, 8vw, 80px) clamp(10px, 3vw, 20px);
padding: 80px 20px;
background-color: #FFFFFF;
}
@ -77,8 +74,8 @@
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
gap: clamp(1.5rem, 3vw, 2.5rem);
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2.5rem;
max-width: 1200px;
margin: 0 auto;
}
@ -89,7 +86,7 @@
}
.feature-icon {
color: var(--dutch-red);
color: var(--primary-color);
margin-bottom: 1.5rem;
display: inline-block;
}
@ -107,141 +104,10 @@
line-height: 1.6;
}
/* Pricing Calculator Section */
.pricing-calculator {
padding: clamp(30px, 8vw, 80px) clamp(10px, 3vw, 20px);
background: #FFFFFF;
text-align: center;
}
.calculator-content {
max-width: 600px;
margin: 0 auto;
}
.pricing-calculator h2 {
font-size: 2.25rem;
color: var(--text-color);
margin-bottom: 0.75rem;
}
.calculator-subtitle {
font-size: 1rem;
color: var(--light-text-color);
margin-bottom: 3rem;
}
.storage-display {
margin-bottom: 2rem;
}
.storage-value {
font-size: clamp(2rem, 8vw, 4rem);
font-weight: 700;
color: var(--dutch-blue);
}
.storage-unit {
font-size: clamp(1.25rem, 4vw, 2rem);
color: var(--light-text-color);
margin-left: 0.5rem;
}
.slider-container {
margin-bottom: 3rem;
}
#storageSlider {
width: 100%;
height: 8px;
border-radius: 4px;
background: linear-gradient(to right, var(--dutch-red), var(--dutch-blue));
outline: none;
-webkit-appearance: none;
appearance: none;
cursor: pointer;
}
#storageSlider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--dutch-blue);
cursor: pointer;
border: 3px solid #FFFFFF;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
#storageSlider::-moz-range-thumb {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--dutch-blue);
cursor: pointer;
border: 3px solid #FFFFFF;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.slider-labels {
display: flex;
justify-content: space-between;
margin-top: 0.5rem;
font-size: 0.875rem;
color: var(--light-text-color);
}
.price-display {
margin-bottom: 2.5rem;
padding: clamp(1rem, 3vw, 2rem);
background: linear-gradient(135deg, rgba(33, 70, 139, 0.05) 0%, rgba(174, 28, 40, 0.05) 100%);
border-radius: 12px;
border: 2px solid var(--dutch-blue);
}
.price-amount {
display: flex;
align-items: baseline;
justify-content: center;
margin-bottom: 0.5rem;
flex-wrap: wrap;
}
.currency {
font-size: clamp(1.25rem, 4vw, 2rem);
color: var(--text-color);
margin-right: 0.25rem;
}
.price-value {
font-size: clamp(2rem, 8vw, 4rem);
font-weight: 700;
color: var(--dutch-red);
}
.price-period {
font-size: clamp(0.875rem, 2.5vw, 1.25rem);
color: var(--light-text-color);
margin-left: 0.5rem;
}
.price-description {
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
color: var(--dutch-blue);
font-weight: 600;
margin: 0;
}
.pricing-calculator .cta-btn {
padding: clamp(0.75rem, 2vw, 1rem) clamp(1.5rem, 5vw, 3rem);
font-size: clamp(0.875rem, 2.5vw, 1.125rem);
}
/* Use Cases Section */
.use-cases-section {
padding: clamp(30px, 8vw, 80px) clamp(10px, 3vw, 20px);
background: linear-gradient(135deg, rgba(174, 28, 40, 0.02) 0%, rgba(33, 70, 139, 0.02) 100%);
padding: 80px 20px;
background: linear-gradient(135deg, #F7FAFC 0%, #EDF2F7 100%);
text-align: center;
}
@ -253,8 +119,8 @@
.use-cases-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 280px), 1fr));
gap: clamp(1.5rem, 3vw, 2rem);
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
max-width: 1200px;
margin: 0 auto;
}
@ -269,9 +135,7 @@
.use-case-card:hover {
transform: translateY(-8px);
box-shadow: 0 8px 30px rgba(174, 28, 40, 0.15);
border-left: 4px solid var(--dutch-red);
border-right: 4px solid var(--dutch-blue);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.use-case-icon {
@ -295,11 +159,10 @@
/* CTA Section */
.cta-section {
padding: clamp(40px, 10vw, 100px) clamp(10px, 3vw, 20px);
background: linear-gradient(135deg, var(--dutch-blue) 0%, var(--dutch-blue-dark) 100%);
padding: 100px 20px;
background: linear-gradient(135deg, var(--primary-color) 0%, #0052CC 100%);
text-align: center;
color: var(--dutch-white);
border-top: 6px solid var(--dutch-red);
color: white;
}
.cta-content {
@ -309,13 +172,13 @@
.cta-section h2 {
font-size: 2.75rem;
color: var(--dutch-white);
color: white;
margin-bottom: 1rem;
}
.cta-section p {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.95);
color: rgba(255, 255, 255, 0.9);
margin-bottom: 2.5rem;
}
@ -323,20 +186,17 @@
padding: 1rem 2.5rem;
font-size: 1.125rem;
font-weight: 600;
background-color: var(--dutch-white);
color: var(--dutch-blue);
background-color: white;
color: var(--primary-color);
border-radius: 8px;
text-decoration: none;
display: inline-block;
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease;
border: 2px solid var(--dutch-red);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(174, 28, 40, 0.3);
background-color: var(--dutch-red);
color: var(--dutch-white);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2);
}
/* Responsive Design */
@ -399,135 +259,17 @@
}
@media (max-width: 480px) {
.hero-section {
padding: 40px 15px 30px;
}
.hero-section h1 {
font-size: 1.75rem;
font-size: 2rem;
}
.hero-subtitle {
font-size: 0.95rem;
font-size: 1rem;
}
.features-header h2,
.use-cases-section h2,
.cta-section h2 {
font-size: 1.5rem;
}
.pricing-calculator {
padding: 40px 15px;
}
.pricing-calculator h2 {
font-size: 1.5rem;
}
.calculator-subtitle {
font-size: 0.9rem;
margin-bottom: 2rem;
}
.storage-display {
margin-bottom: 1.5rem;
}
.slider-container {
margin-bottom: 2rem;
}
.features-section,
.use-cases-section {
padding: 40px 15px;
}
.cta-section {
padding: 40px 15px;
}
.cta-section h2 {
font-size: 1.75rem;
}
.cta-section p {
font-size: 1rem;
}
}
@media (max-width: 360px) {
.hero-section {
padding: 30px 10px 20px;
}
.hero-section h1 {
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
.hero-subtitle {
font-size: 0.875rem;
}
.pricing-calculator {
padding: 30px 10px;
}
.pricing-calculator h2 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.calculator-subtitle {
font-size: 0.85rem;
margin-bottom: 1.5rem;
}
.storage-display {
margin-bottom: 1rem;
}
.slider-container {
margin-bottom: 1.5rem;
}
.price-display {
margin-bottom: 1.5rem;
padding: 0.75rem;
}
.features-section,
.use-cases-section {
padding: 30px 10px;
}
.features-grid {
gap: 1.5rem;
}
.feature-card {
padding: 1.5rem;
}
.use-cases-grid {
gap: 1.5rem;
}
.use-case-card {
padding: 1.5rem;
}
.cta-section {
padding: 30px 10px;
}
.cta-section h2 {
font-size: 1.5rem;
}
.cta-section p {
font-size: 0.9rem;
margin-bottom: 1.5rem;
}
}

View File

@ -4,33 +4,31 @@
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: clamp(30px, 5vh, 60px) clamp(15px, 3vw, 20px) clamp(40px, 6vh, 80px);
justify-content: center;
min-height: calc(100vh - 120px); /* Adjust based on header/footer height */
padding: 20px;
box-sizing: border-box;
width: 100%;
max-width: 100%;
}
.login-page-container h1 {
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-size: 2.5rem;
color: var(--text-color);
margin-bottom: 10px;
text-align: center;
}
.login-page-container .subtitle {
font-size: clamp(0.95rem, 2vw, 1.1rem);
font-size: 1.1rem;
color: var(--light-text-color);
margin-bottom: clamp(20px, 4vh, 30px);
margin-bottom: 30px;
text-align: center;
}
.form-container {
max-width: 450px;
max-width: 450px; /* Adjust as needed */
width: 100%;
padding: clamp(1.5rem, 4vw, 2.5rem);
padding: 2.5rem;
box-sizing: border-box;
margin-bottom: clamp(20px, 4vh, 30px);
}
.forgot-password-link {
@ -44,7 +42,7 @@
}
.forgot-password-link:hover {
color: var(--dutch-red);
color: var(--accent-color);
text-decoration: underline;
}
@ -62,46 +60,19 @@
}
.create-account-link a:hover {
color: var(--dutch-red);
color: var(--accent-color);
text-decoration: underline;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.login-page-container {
padding: 30px 15px 40px;
}
.login-page-container h1 {
font-size: 1.75rem;
font-size: 2rem;
}
.login-page-container .subtitle {
font-size: 0.95rem;
margin-bottom: 20px;
font-size: 1rem;
}
.form-container {
padding: 1.5rem;
}
}
@media (max-width: 360px) {
.login-page-container {
padding: 20px 10px 30px;
}
.login-page-container h1 {
font-size: 1.5rem;
margin-bottom: 8px;
}
.login-page-container .subtitle {
font-size: 0.875rem;
margin-bottom: 15px;
}
.form-container {
padding: 1rem;
}
.forgot-password-link {
font-size: 0.85rem;
}
.create-account-link {
font-size: 0.85rem;
padding: 2rem;
}
}

View File

@ -373,59 +373,4 @@
flex-grow: unset;
width: 100%;
}
}
@media (max-width: 360px) {
.overview-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.quota-overview-card {
padding: 10px;
}
.quota-overview-card h2 {
font-size: 1rem;
}
.quota-overview-card .subtitle {
font-size: 0.8rem;
}
.donut-chart-container {
width: 120px;
height: 120px;
}
.donut-chart-container::before {
width: 90px;
height: 90px;
}
.donut-chart-text {
font-size: 0.85rem;
}
.order-form-card {
padding: 10px;
}
.order-form-card h3 {
font-size: 1rem;
}
.user-quotas-section {
padding: 10px;
}
.user-quotas-header h2 {
font-size: 1rem;
}
.user-quota-list {
grid-template-columns: 1fr;
gap: 15px;
}
.user-quota-item {
padding: 15px;
}
.user-quota-item .user-info h4 {
font-size: 1rem;
}
.modal-content {
padding: 15px;
}
.modal-content h3 {
font-size: 1.1rem;
}
}

View File

@ -0,0 +1,176 @@
/* Styles for the Pricing Page (pricing.html) */
.pricing-hero {
text-align: center;
padding: 40px 20px;
margin-bottom: 40px;
}
.pricing-hero h1 {
font-size: 3rem;
color: var(--text-color);
margin-bottom: 1rem;
}
.pricing-hero p {
font-size: 1.2rem;
color: var(--light-text-color);
margin-bottom: 30px;
}
.pricing-toggle {
display: inline-flex;
border-radius: 5px;
overflow: hidden;
border: 1px solid var(--border-color);
}
.pricing-toggle .btn-toggle {
padding: 10px 20px;
border: none;
background-color: var(--card-background);
color: var(--text-color);
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s ease, color 0.3s ease;
}
.pricing-toggle .btn-toggle.active {
background-color: var(--primary-color);
color: white;
}
.pricing-toggle .btn-toggle:hover:not(.active) {
background-color: var(--background-color);
}
.pricing-tiers {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
max-width: 1200px;
margin: 0 auto 60px auto;
padding: 0 20px;
}
.pricing-card {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 15px var(--shadow-color);
padding: 30px;
text-align: center;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
.pricing-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.pricing-card.featured {
border: 2px solid var(--primary-color);
transform: scale(1.02);
}
.pricing-card h3 {
font-size: 1.8rem;
color: var(--text-color);
margin-bottom: 15px;
}
.pricing-card .price {
font-size: 3.5rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 20px;
}
.pricing-card .price span {
font-size: 1.2rem;
font-weight: 400;
color: var(--light-text-color);
}
.pricing-card ul {
list-style: none;
padding: 0;
margin-bottom: 30px;
text-align: left;
}
.pricing-card ul li {
margin-bottom: 10px;
color: var(--text-color);
font-size: 1rem;
display: flex;
align-items: center;
}
.pricing-card ul li::before {
content: 'âś“'; /* Checkmark icon */
color: var(--accent-color);
margin-right: 10px;
font-weight: bold;
}
.pricing-card .btn-primary {
margin-top: auto; /* Push button to the bottom */
width: 100%;
padding: 12px 20px;
font-size: 1.1rem;
}
.pricing-faq {
max-width: 800px;
margin: 0 auto 60px auto;
padding: 0 20px;
}
.pricing-faq h2 {
text-align: center;
font-size: 2.5rem;
color: var(--text-color);
margin-bottom: 40px;
}
.faq-item {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 2px 10px var(--shadow-color);
padding: 20px;
margin-bottom: 20px;
}
.faq-item h3 {
font-size: 1.3rem;
color: var(--text-color);
margin-bottom: 10px;
}
.faq-item p {
font-size: 1rem;
color: var(--light-text-color);
line-height: 1.6;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.pricing-hero h1 {
font-size: 2.5rem;
}
.pricing-hero p {
font-size: 1rem;
}
.pricing-tiers {
grid-template-columns: 1fr;
}
.pricing-card.featured {
transform: none; /* Remove scale on small screens */
}
.pricing-faq h2 {
font-size: 2rem;
}
}

View File

@ -4,33 +4,31 @@
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
padding: clamp(30px, 5vh, 60px) clamp(15px, 3vw, 20px) clamp(40px, 6vh, 80px);
justify-content: center;
min-height: calc(100vh - 120px); /* Adjust based on header/footer height */
padding: 20px;
box-sizing: border-box;
width: 100%;
max-width: 100%;
}
.register-page-container h1 {
font-size: clamp(1.75rem, 4vw, 2.5rem);
font-size: 2.5rem;
color: var(--text-color);
margin-bottom: 10px;
text-align: center;
}
.register-page-container .subtitle {
font-size: clamp(0.95rem, 2vw, 1.1rem);
font-size: 1.1rem;
color: var(--light-text-color);
margin-bottom: clamp(20px, 4vh, 30px);
margin-bottom: 30px;
text-align: center;
}
.form-container {
max-width: 450px;
max-width: 450px; /* Adjust as needed */
width: 100%;
padding: clamp(1.5rem, 4vw, 2.5rem);
padding: 2.5rem;
box-sizing: border-box;
margin-bottom: clamp(20px, 4vh, 30px);
}
.terms-checkbox {
@ -46,16 +44,16 @@
margin-right: 10px;
width: 18px;
height: 18px;
accent-color: var(--dutch-red);
accent-color: var(--accent-color); /* Style checkbox with accent color */
}
.terms-checkbox a {
color: var(--dutch-blue);
color: var(--primary-color);
font-weight: 500;
}
.terms-checkbox a:hover {
color: var(--dutch-red);
color: var(--accent-color);
text-decoration: underline;
}
@ -73,46 +71,19 @@
}
.login-link a:hover {
color: var(--dutch-red);
color: var(--accent-color);
text-decoration: underline;
}
/* Responsive adjustments */
@media (max-width: 600px) {
.register-page-container {
padding: 30px 15px 40px;
}
.register-page-container h1 {
font-size: 1.75rem;
font-size: 2rem;
}
.register-page-container .subtitle {
font-size: 0.95rem;
margin-bottom: 20px;
font-size: 1rem;
}
.form-container {
padding: 1.5rem;
}
}
@media (max-width: 360px) {
.register-page-container {
padding: 20px 10px 30px;
}
.register-page-container h1 {
font-size: 1.5rem;
margin-bottom: 8px;
}
.register-page-container .subtitle {
font-size: 0.875rem;
margin-bottom: 15px;
}
.form-container {
padding: 1rem;
}
.terms-checkbox {
font-size: 0.85rem;
}
.login-link {
font-size: 0.85rem;
padding: 2rem;
}
}

View File

@ -0,0 +1,142 @@
/* Styles for the Security Page (security.html) */
.security-hero {
text-align: center;
padding: 40px 20px;
margin-bottom: 60px;
}
.security-hero h1 {
font-size: 3rem;
color: var(--text-color);
margin-bottom: 1rem;
}
.security-hero p {
font-size: 1.2rem;
color: var(--light-text-color);
max-width: 800px;
margin: 0 auto;
}
.security-pillars {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 30px;
max-width: 1200px;
margin: 0 auto 60px auto;
padding: 0 20px;
}
.pillar-card {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 15px var(--shadow-color);
padding: 30px;
text-align: center;
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
.pillar-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.pillar-card img.icon {
width: 80px;
height: 80px;
margin-bottom: 20px;
}
.pillar-card h3 {
font-size: 1.5rem;
color: var(--text-color);
margin-bottom: 15px;
}
.pillar-card p {
font-size: 1rem;
color: var(--light-text-color);
line-height: 1.6;
}
.security-certifications {
text-align: center;
margin-bottom: 60px;
}
.security-certifications h2 {
font-size: 2.5rem;
color: var(--text-color);
margin-bottom: 40px;
}
.cert-grid {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 40px;
}
.cert-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.cert-item img.cert-logo {
height: 60px;
width: auto;
}
.cert-item p {
font-size: 1.1rem;
color: var(--text-color);
font-weight: 500;
}
.security-cta {
text-align: center;
padding: 40px 20px;
background-color: var(--background-color);
border-radius: 8px;
margin-bottom: 40px;
}
.security-cta h2 {
font-size: 2rem;
color: var(--text-color);
margin-bottom: 30px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.security-hero h1 {
font-size: 2.5rem;
}
.security-hero p {
font-size: 1rem;
}
.security-certifications h2 {
font-size: 2rem;
}
.security-cta h2 {
font-size: 1.8rem;
}
}
@media (max-width: 480px) {
.security-hero h1 {
font-size: 2rem;
}
.pillar-card {
padding: 20px;
}
.cert-grid {
gap: 20px;
}
.cert-item img.cert-logo {
height: 50px;
}
}

View File

@ -0,0 +1,135 @@
/* Styles for the Solutions Page (solutions.html) */
.solutions-hero {
text-align: center;
padding: 40px 20px;
margin-bottom: 60px;
}
.solutions-hero h1 {
font-size: 3rem;
color: var(--text-color);
margin-bottom: 1rem;
}
.solutions-hero p {
font-size: 1.2rem;
color: var(--light-text-color);
max-width: 800px;
margin: 0 auto;
}
.solution-sections {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
max-width: 1200px;
margin: 0 auto 60px auto;
padding: 0 20px;
}
.solution-card {
background-color: var(--card-background);
border-radius: 8px;
box-shadow: 0 4px 15px var(--shadow-color);
padding: 30px;
text-align: left;
display: flex;
flex-direction: column;
transition: transform 0.3s ease-in-out, box-shadow 0.3s ease-in-out;
}
.solution-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.solution-card img.icon {
width: 60px;
height: 60px;
margin-bottom: 20px;
align-self: center; /* Center the icon */
}
.solution-card h2 {
font-size: 1.8rem;
color: var(--text-color);
margin-bottom: 15px;
text-align: center;
}
.solution-card p {
font-size: 1rem;
color: var(--light-text-color);
line-height: 1.6;
margin-bottom: 20px;
}
.solution-card ul {
list-style: none;
padding: 0;
margin-bottom: 20px;
}
.solution-card ul li {
margin-bottom: 8px;
color: var(--text-color);
font-size: 0.95rem;
display: flex;
align-items: flex-start;
}
.solution-card ul li::before {
content: '•'; /* Bullet point */
color: var(--accent-color);
margin-right: 10px;
font-weight: bold;
font-size: 1.2em;
line-height: 1;
}
.solution-card .btn-primary {
margin-top: auto; /* Push button to the bottom */
align-self: center; /* Center the button */
padding: 10px 20px;
font-size: 1rem;
}
.solutions-cta {
text-align: center;
padding: 40px 20px;
background-color: var(--background-color);
border-radius: 8px;
margin-bottom: 40px;
}
.solutions-cta h2 {
font-size: 2rem;
color: var(--text-color);
margin-bottom: 30px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.solutions-hero h1 {
font-size: 2.5rem;
}
.solutions-hero p {
font-size: 1rem;
}
.solution-sections {
grid-template-columns: 1fr;
}
.solutions-cta h2 {
font-size: 1.8rem;
}
}
@media (max-width: 480px) {
.solutions-hero h1 {
font-size: 2rem;
}
.solution-card {
padding: 20px;
}
}

View File

@ -169,53 +169,7 @@
.support-hero h1, .support-categories h2, .contact-options h2 {
font-size: 1.2rem;
}
.support-hero p {
font-size: 0.85rem;
}
.category-card, .contact-card {
padding: 15px;
}
.search-support .search-input {
font-size: 0.85rem;
}
}
@media (max-width: 360px) {
.support-hero {
padding: 0;
margin-bottom: 20px;
}
.support-hero h1, .support-categories h2, .contact-options h2 {
font-size: 1.1rem;
}
.support-hero p {
font-size: 0.8rem;
}
.search-support {
max-width: 100%;
margin-bottom: 20px;
}
.search-support .search-input {
padding: 8px 12px;
font-size: 0.8rem;
}
.support-categories {
margin-bottom: 30px;
}
.category-grid, .contact-grid {
grid-template-columns: 1fr;
gap: 15px;
}
.category-card, .contact-card {
padding: 12px;
}
.category-card h3, .contact-card h3 {
font-size: 1rem;
}
.category-card p, .contact-card p {
font-size: 0.85rem;
}
.contact-options {
margin-bottom: 30px;
}
}

View File

@ -2,18 +2,18 @@
.use-cases-hero {
text-align: center;
padding: clamp(20px, 5vw, 40px) clamp(10px, 3vw, 20px);
margin-bottom: clamp(30px, 6vw, 60px);
padding: 40px 20px;
margin-bottom: 60px;
}
.use-cases-hero h1 {
font-size: clamp(1.75rem, 5vw, 3rem);
font-size: 3rem;
color: var(--text-color);
margin-bottom: 1rem;
}
.use-cases-hero p {
font-size: clamp(0.95rem, 2.5vw, 1.2rem);
font-size: 1.2rem;
color: var(--light-text-color);
max-width: 800px;
margin: 0 auto;
@ -21,11 +21,11 @@
.use-case-scenarios {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
gap: clamp(20px, 3vw, 30px);
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
max-width: 1200px;
margin: 0 auto clamp(30px, 6vw, 60px) auto;
padding: 0 clamp(10px, 3vw, 20px);
margin: 0 auto 60px auto;
padding: 0 20px;
}
.scenario-card {
@ -127,52 +127,9 @@
@media (max-width: 480px) {
.use-cases-hero h1 {
font-size: 1.75rem;
}
.use-cases-hero p {
font-size: 0.9rem;
font-size: 2rem;
}
.scenario-card {
padding: 20px;
}
.scenario-card h2 {
font-size: 1.5rem;
}
.use-cases-cta h2 {
font-size: 1.5rem;
}
}
@media (max-width: 360px) {
.use-cases-hero {
padding: 15px 10px;
margin-bottom: 20px;
}
.use-cases-hero h1 {
font-size: 1.5rem;
}
.use-cases-hero p {
font-size: 0.85rem;
}
.use-case-scenarios {
gap: 20px;
padding: 0 10px;
grid-template-columns: 1fr;
min-width: 0;
}
.scenario-card {
padding: 15px;
}
.scenario-card h2 {
font-size: 1.25rem;
}
.scenario-card p {
font-size: 0.9rem;
}
.use-cases-cta {
padding: 20px 10px;
}
.use-cases-cta h2 {
font-size: 1.25rem;
}
}

View File

@ -88,9 +88,9 @@ document.addEventListener('DOMContentLoaded', () => {
</div>
<div class="quota-actions">
<button class="btn-outline edit-quota-btn" data-email="${user.email}" data-quota="${user.storage_quota_gb}">Edit Quota</button>
<button class="btn-outline delete-user-btn" data-email="${user.email}">🗑️</button>
<button class="btn-outline delete-user-btn" data-email="${user.email}">Delete User</button>
<button class="btn-outline view-details-btn" data-email="${user.email}">View Details</button>
${user.parent_email === null ? `<button class="btn-outline delete-team-btn" data-parent-email="${user.email}">🗑️</button>` : ''}
${user.parent_email === null ? `<button class="btn-outline delete-team-btn" data-parent-email="${user.email}">Delete Team</button>` : ''}
</div>
`;
userQuotaList.appendChild(quotaItem);

View File

@ -1,79 +0,0 @@
class PricingCalculator {
constructor() {
this.slider = document.getElementById('storageSlider');
this.storageValue = document.getElementById('storageValue');
this.priceValue = document.getElementById('priceValue');
this.planDescription = document.getElementById('planDescription');
this.pricingTiers = [
{ maxGB: 10, price: 0, name: 'Free Plan' },
{ maxGB: 100, price: 9, name: 'Personal Plan' },
{ maxGB: 500, price: 29, name: 'Professional Plan' },
{ maxGB: 1000, price: 49, name: 'Business Plan' },
{ maxGB: 2000, price: 99, name: 'Enterprise Plan' }
];
this.init();
}
init() {
if (!this.slider) return;
this.slider.addEventListener('input', () => this.updatePrice());
this.updatePrice();
}
calculatePrice(storageGB) {
for (let tier of this.pricingTiers) {
if (storageGB <= tier.maxGB) {
return {
price: tier.price,
plan: tier.name
};
}
}
return {
price: 99,
plan: 'Enterprise Plan'
};
}
formatStorage(value) {
if (value >= 1000) {
return `${(value / 1000).toFixed(1)} TB`;
}
return `${value} GB`;
}
updatePrice() {
const storage = parseInt(this.slider.value);
const pricing = this.calculatePrice(storage);
if (storage >= 1000) {
this.storageValue.textContent = (storage / 1000).toFixed(1);
this.storageValue.nextElementSibling.textContent = 'TB';
} else {
this.storageValue.textContent = storage;
this.storageValue.nextElementSibling.textContent = 'GB';
}
this.priceValue.textContent = pricing.price;
this.planDescription.textContent = pricing.plan;
}
}
class Application {
constructor() {
this.pricingCalculator = null;
this.init();
}
init() {
document.addEventListener('DOMContentLoaded', () => {
this.pricingCalculator = new PricingCalculator();
});
}
}
const app = new Application();
export default app;

View File

@ -34,8 +34,8 @@ document.addEventListener('DOMContentLoaded', () => {
<div class="quota-actions">
<a href="/users/${user.email}/details" class="btn-outline">View Details</a>
<a href="/users/${user.email}/edit" class="btn-outline">Edit Quota</a>
<button class="btn-outline delete-user-btn" data-email="${user.email}">🗑️</button>
${user.parent_email === null ? `<button class="btn-outline delete-team-btn" data-parent-email="${user.email}">🗑️</button>` : ''}
<button class="btn-outline delete-user-btn" data-email="${user.email}">Delete User</button>
${user.parent_email === null ? `<button class="btn-outline delete-team-btn" data-parent-email="${user.email}">Delete Team</button>` : ''}
</div>
`;
userQuotaList.appendChild(quotaItem);

View File

@ -115,65 +115,62 @@ document.addEventListener('DOMContentLoaded', () => {
async function shareFile(paths, names) {
const modal = document.getElementById('share-modal');
const linkContainer = document.getElementById('share-link-container');
const loading = document.getElementById('share-loading');
const shareLinkInput = document.getElementById('share-link-input');
const shareFileName = document.getElementById('share-file-name');
const quickShareResult = document.getElementById('quick-share-result');
const quickShareLinkInput = document.getElementById('quick-share-link-input');
const generateQuickShareBtn = document.getElementById('generate-quick-share-btn');
const advancedShareBtn = document.getElementById('advanced-share-btn');
const shareLinksList = document.getElementById('share-links-list');
quickShareResult.style.display = 'none';
quickShareLinkInput.value = '';
shareLinkInput.value = '';
if (shareLinksList) shareLinksList.innerHTML = '';
linkContainer.style.display = 'none';
loading.style.display = 'block';
modal.classList.add('show');
const currentPath = paths[0];
const currentName = names[0];
if (paths.length === 1) {
shareFileName.textContent = `Sharing: ${currentName}`;
shareFileName.textContent = `Sharing: ${names[0]}`;
} else {
shareFileName.textContent = `Sharing ${paths.length} items`;
}
generateQuickShareBtn.onclick = async function() {
generateQuickShareBtn.disabled = true;
generateQuickShareBtn.textContent = 'Generating...';
try {
const response = await fetch(`/files/share_multiple`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ paths: paths })
});
const data = await response.json();
try {
const response = await fetch('/api/sharing/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
item_path: currentPath,
permission: 'view',
scope: 'public',
expiration_days: 7
})
});
const data = await response.json();
if (data.success) {
quickShareLinkInput.value = data.share_url;
quickShareResult.style.display = 'block';
generateQuickShareBtn.textContent = 'Generate Another Link';
if (data.share_links && data.share_links.length > 0) {
if (data.share_links.length === 1) {
shareLinkInput.value = data.share_links[0];
linkContainer.style.display = 'block';
} else {
alert('Error generating share link: ' + data.error);
generateQuickShareBtn.textContent = 'Generate Quick Share Link';
// Display multiple links
if (!shareLinksList) {
// Create the list if it doesn't exist
const ul = document.createElement('ul');
ul.id = 'share-links-list';
linkContainer.appendChild(ul);
shareLinksList = ul;
}
data.share_links.forEach(item => {
const li = document.createElement('li');
li.innerHTML = `<strong>${item.name}:</strong> <input type="text" value="${item.link}" readonly class="form-input share-link-item-input"> <button class="btn-primary copy-share-link-item-btn" data-link="${item.link}">Copy</button>`;
shareLinksList.appendChild(li);
});
linkContainer.style.display = 'block';
}
} catch (error) {
console.error('Error generating share link:', error);
alert('Error generating share link');
generateQuickShareBtn.textContent = 'Generate Quick Share Link';
} finally {
generateQuickShareBtn.disabled = false;
loading.style.display = 'none';
} else {
loading.textContent = 'Error generating share link(s)';
}
};
advancedShareBtn.onclick = function() {
window.location.href = `/sharing/create?item_path=${encodeURIComponent(currentPath)}`;
};
} catch (error) {
console.error('Error sharing files:', error);
loading.textContent = 'Error generating share link(s)';
}
}
function copyShareLink() {
@ -298,20 +295,6 @@ document.addEventListener('DOMContentLoaded', () => {
copyShareLinkBtn.addEventListener('click', copyShareLink);
}
const copyQuickShareBtn = document.getElementById('copy-quick-share-btn');
if (copyQuickShareBtn) {
copyQuickShareBtn.addEventListener('click', function() {
const input = document.getElementById('quick-share-link-input');
input.select();
navigator.clipboard.writeText(input.value).then(() => {
alert('Share link copied to clipboard');
}).catch(err => {
document.execCommand('copy');
alert('Share link copied to clipboard');
});
});
}
document.getElementById('select-all')?.addEventListener('change', function(e) {
const checkboxes = document.querySelectorAll('.file-checkbox');
checkboxes.forEach(cb => cb.checked = e.target.checked);

View File

@ -5,12 +5,15 @@
<span class="brand-text">Retoor's</span>
</a>
<ul class="nav-menu">
{% if request.get('user') %}
<li><a href="/solutions">Solutions</a></li>
<li><a href="/pricing">Pricing</a></li>
<li><a href="/security">Security</a></li>
{% if request['user'] %}
<li><a href="/files">My Files</a></li>
{% endif %}
</ul>
<div class="nav-actions">
{% if request.get('user') %}
{% if request['user'] %}
<a href="/files" class="nav-link">Dashboard</a>
<a href="/logout" class="btn-outline">Logout</a>
{% else %}

View File

@ -11,7 +11,6 @@
<div class="sidebar-menu">
<ul>
<li><a href="/files" {% if active_page == 'files' %}class="active"{% endif %}><img src="/static/images/icon-families.svg" alt="My Files Icon" class="icon"> My Files</a></li>
<li><a href="/sharing/manage" {% if active_page == 'my_shares' %}class="active"{% endif %}><img src="/static/images/icon-professionals.svg" alt="My Shares Icon" class="icon"> My Shares</a></li>
<li><a href="/shared" {% if active_page == 'shared' %}class="active"{% endif %}><img src="/static/images/icon-professionals.svg" alt="Shared Icon" class="icon"> Shared with me</a></li>
<li><a href="/recent" {% if active_page == 'recent' %}class="active"{% endif %}><img src="/static/images/icon-students.svg" alt="Recent Icon" class="icon"> Recent</a></li>
<li><a href="/favorites" {% if active_page == 'favorites' %}class="active"{% endif %}><img src="/static/images/icon-families.svg" alt="Favorites Icon" class="icon"> Favorites</a></li>

View File

@ -1,246 +0,0 @@
{% extends "layouts/dashboard.html" %}
{% block title %}Create Share - Retoor's Cloud Solutions{% endblock %}
{% block dashboard_head %}
<link rel="stylesheet" href="/static/css/components/form.css">
<style>
.share-form-container {
max-width: 800px;
margin: 2rem auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.form-section {
margin-bottom: 2rem;
}
.form-section h3 {
margin-bottom: 1rem;
color: #333;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input, .form-group select, .form-group textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox-group input {
width: auto;
}
.recipient-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.recipient-item {
display: flex;
gap: 0.5rem;
align-items: center;
}
.btn-add-recipient {
margin-top: 0.5rem;
}
.share-url-display {
display: none;
margin-top: 2rem;
padding: 1rem;
background: #f0f7ff;
border-radius: 4px;
}
.share-url-display input {
font-family: monospace;
background: white;
}
</style>
{% endblock %}
{% block page_title %}Create Share{% endblock %}
{% block dashboard_content %}
<div class="share-form-container">
<form id="create-share-form">
<div class="form-section">
<h3>Basic Information</h3>
<div class="form-group">
<label for="item_path">Item to Share</label>
<input type="text" id="item_path" name="item_path" value="{{ item_path }}" required readonly>
</div>
<div class="form-group">
<label for="permission">Permission Level</label>
<select id="permission" name="permission">
<option value="view">View Only - Can view and download</option>
<option value="edit">Edit - Can modify, add, and delete</option>
<option value="comment">Comment - Can provide feedback only</option>
</select>
</div>
<div class="form-group">
<label for="scope">Sharing Scope</label>
<select id="scope" name="scope">
<option value="public">Public - Anyone with the link</option>
<option value="private">Private - Specific recipients only</option>
<option value="account_based">Account Based - Requires user account</option>
</select>
</div>
</div>
<div class="form-section" id="recipients-section" style="display: none;">
<h3>Recipients</h3>
<div class="form-group">
<label>Recipient Emails</label>
<div class="recipient-list" id="recipient-list">
<div class="recipient-item">
<input type="email" class="recipient-email" placeholder="email@example.com">
<button type="button" class="btn-small btn-remove-recipient">Remove</button>
</div>
</div>
<button type="button" class="btn-outline btn-add-recipient" id="add-recipient-btn">Add Recipient</button>
</div>
</div>
<div class="form-section">
<h3>Security Options</h3>
<div class="form-group">
<label for="password">Password Protection</label>
<input type="password" id="password" name="password" placeholder="Optional password">
</div>
<div class="form-group">
<label for="expiration_days">Expiration</label>
<select id="expiration_days" name="expiration_days">
<option value="">Never expires</option>
<option value="1">1 day</option>
<option value="7" selected>7 days</option>
<option value="30">30 days</option>
<option value="90">90 days</option>
</select>
</div>
<div class="form-group checkbox-group">
<input type="checkbox" id="disable_download" name="disable_download">
<label for="disable_download">Disable downloads (view only)</label>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">Create Share Link</button>
<button type="button" class="btn-outline" onclick="history.back()">Cancel</button>
</div>
</form>
<div class="share-url-display" id="share-url-display">
<h3>Share Link Created</h3>
<div class="form-group">
<label>Share URL</label>
<input type="text" id="share-url" readonly>
<button type="button" class="btn-primary" id="copy-url-btn">Copy Link</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="module">
const form = document.getElementById('create-share-form');
const scopeSelect = document.getElementById('scope');
const recipientsSection = document.getElementById('recipients-section');
const addRecipientBtn = document.getElementById('add-recipient-btn');
const recipientList = document.getElementById('recipient-list');
const shareUrlDisplay = document.getElementById('share-url-display');
const shareUrlInput = document.getElementById('share-url');
const copyUrlBtn = document.getElementById('copy-url-btn');
scopeSelect.addEventListener('change', () => {
if (scopeSelect.value === 'private' || scopeSelect.value === 'account_based') {
recipientsSection.style.display = 'block';
} else {
recipientsSection.style.display = 'none';
}
});
addRecipientBtn.addEventListener('click', () => {
const recipientItem = document.createElement('div');
recipientItem.className = 'recipient-item';
recipientItem.innerHTML = `
<input type="email" class="recipient-email" placeholder="email@example.com">
<button type="button" class="btn-small btn-remove-recipient">Remove</button>
`;
recipientList.appendChild(recipientItem);
});
recipientList.addEventListener('click', (e) => {
if (e.target.classList.contains('btn-remove-recipient')) {
e.target.parentElement.remove();
}
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
const formData = {
item_path: document.getElementById('item_path').value,
permission: document.getElementById('permission').value,
scope: document.getElementById('scope').value,
password: document.getElementById('password').value || null,
expiration_days: parseInt(document.getElementById('expiration_days').value) || null,
disable_download: document.getElementById('disable_download').checked,
recipient_emails: []
};
if (formData.scope === 'private' || formData.scope === 'account_based') {
const emailInputs = document.querySelectorAll('.recipient-email');
formData.recipient_emails = Array.from(emailInputs)
.map(input => input.value.trim())
.filter(email => email);
}
try {
const response = await fetch('/api/sharing/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
if (result.success) {
shareUrlInput.value = result.share_url;
shareUrlDisplay.style.display = 'block';
form.style.display = 'none';
} else {
alert('Error: ' + result.error);
}
} catch (error) {
alert('Error creating share: ' + error.message);
}
});
copyUrlBtn.addEventListener('click', () => {
shareUrlInput.select();
document.execCommand('copy');
copyUrlBtn.textContent = 'Copied!';
setTimeout(() => {
copyUrlBtn.textContent = 'Copy Link';
}, 2000);
});
</script>
{% endblock %}

View File

@ -57,7 +57,7 @@
<button class="btn-outline" onclick="alert('Download feature coming soon!')">Download</button>
<button class="btn-outline" onclick="alert('Upload feature coming soon!')">Upload</button>
<button class="btn-outline" onclick="alert('Share feature coming soon!')">Share</button>
<button class="btn-outline" onclick="alert('Delete feature coming soon!')">🗑️</button>
<button class="btn-outline" onclick="alert('Delete feature coming soon!')">Delete</button>
</div>
</div>

View File

@ -71,7 +71,7 @@
<button class="btn-small download-file-btn" data-path="{{ item.path }}">⬇️</button>
{% endif %}
<button class="btn-small share-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">đź”—</button>
<button class="btn-small btn-danger remove-favorite-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">🗑️</button>
<button class="btn-small btn-danger remove-favorite-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">Remove</button>
</div>
</td>
</tr>
@ -111,7 +111,7 @@
<p id="remove-favorite-message"></p>
<form id="remove-favorite-form" method="post">
<div class="modal-actions">
<button type="submit" class="btn-danger">🗑️</button>
<button type="submit" class="btn-danger">Remove</button>
<button type="button" class="btn-outline" onclick="closeModal('remove-favorite-modal')">Cancel</button>
</div>
</form>

View File

@ -13,7 +13,7 @@
<button class="btn-outline" id="upload-btn">Upload</button>
<button class="btn-outline" id="download-selected-btn" disabled>⬇️</button>
<button class="btn-outline" id="share-selected-btn" disabled>đź”—</button>
<button class="btn-outline" id="delete-selected-btn" disabled>🗑️</button>
<button class="btn-outline" id="delete-selected-btn" disabled>Delete</button>
{% endblock %}
{% block dashboard_content %}
@ -79,7 +79,7 @@
<button class="btn-small download-file-btn" data-path="{{ item.path }}">⬇️</button>
{% endif %}
<button class="btn-small share-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">đź”—</button>
<button class="btn-small btn-danger delete-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">🗑️</button>
<button class="btn-small btn-danger delete-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">Delete</button>
</div>
</td>
</tr>
@ -131,46 +131,20 @@
<div id="share-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('share-modal')">&times;</span>
<h3>Share Item</h3>
<h3>Share File</h3>
<p id="share-file-name"></p>
<div class="share-options">
<h4>Quick Share (Public Link)</h4>
<p style="color: #666; font-size: 0.9em; margin-bottom: 1rem;">Generate a simple public link that anyone can access</p>
<div id="quick-share-container">
<button class="btn-primary" id="generate-quick-share-btn">Generate Quick Share Link</button>
<div id="quick-share-result" style="display: none; margin-top: 1rem;">
<input type="text" id="quick-share-link-input" readonly class="form-input">
<button class="btn-outline" id="copy-quick-share-btn">Copy Link</button>
</div>
</div>
<hr style="margin: 2rem 0;">
<h4>Advanced Sharing</h4>
<p style="color: #666; font-size: 0.9em; margin-bottom: 1rem;">Configure permissions, passwords, expiration, and more</p>
<button class="btn-primary" id="advanced-share-btn">Create Advanced Share</button>
<div id="share-link-container" style="display: none;">
<input type="text" id="share-link-input" readonly class="form-input">
<button class="btn-primary" id="copy-share-link-btn">Copy Link</button>
<div id="share-links-list" class="share-links-list"></div>
</div>
<div class="modal-actions" style="margin-top: 2rem;">
<div id="share-loading">Generating share link...</div>
<div class="modal-actions">
<button type="button" class="btn-outline" onclick="closeModal('share-modal')">Close</button>
</div>
</div>
</div>
<style>
.share-options {
margin: 1.5rem 0;
}
.share-options h4 {
margin-bottom: 0.5rem;
color: #333;
}
#quick-share-result input {
margin-bottom: 0.5rem;
}
</style>
<div id="delete-modal" class="modal">
<div class="modal-content">
<span class="close" onclick="closeModal('delete-modal')">&times;</span>
@ -178,7 +152,7 @@
<p id="delete-message"></p>
<form id="delete-form" method="post">
<div class="modal-actions">
<button type="submit" class="btn-danger">🗑️</button>
<button type="submit" class="btn-danger">Delete</button>
<button type="button" class="btn-outline" onclick="closeModal('delete-modal')">Cancel</button>
</div>
</form>

View File

@ -1,6 +1,6 @@
{% extends "layouts/base.html" %}
{% block title %}Secure Cloud Storage - Retoor's Cloud Solutions{% endblock %}
{% block title %}Secure Cloud Storage for Everyone - Retoor's Cloud Solutions{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/components/index.css">
@ -10,44 +10,113 @@
<main>
<section class="hero-section">
<div class="hero-content">
<h1>Secure Cloud Storage</h1>
<p class="hero-subtitle">Store, sync, and share your files with enterprise-grade security.</p>
<h1>Your files, safe and accessible everywhere</h1>
<p class="hero-subtitle">Store, sync, and share your files with enterprise-grade security. Access from any device, anytime, anywhere.</p>
<div class="hero-ctas">
<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="pricing-calculator">
<div class="calculator-content">
<h2>Calculate Your Price</h2>
<p class="calculator-subtitle">Adjust the slider to find the perfect storage plan</p>
<div class="storage-display">
<span class="storage-value" id="storageValue">100</span>
<span class="storage-unit">GB</span>
</div>
<div class="slider-container">
<input type="range" id="storageSlider" min="1" max="2000" value="100" step="1">
<div class="slider-labels">
<span>1 GB</span>
<span>2 TB</span>
<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="price-display">
<div class="price-amount">
<span class="currency">$</span>
<span class="price-value" id="priceValue">9</span>
<span class="price-period">/month</span>
<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>
<p class="price-description" id="planDescription">Personal Plan</p>
<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>
<a href="/register" class="btn-primary cta-btn">Get Started</a>
<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>
<p>Keep precious memories safe. Share photos and videos with family members securely.</p>
</div>
<div class="use-case-card">
<img src="/static/images/icon-professionals.svg" alt="Professionals Icon" class="use-case-icon">
<h3>For Professionals</h3>
<p>Collaborate on projects, share documents, and work from anywhere with confidence.</p>
</div>
<div class="use-case-card">
<img src="/static/images/icon-students.svg" alt="Students Icon" class="use-case-icon">
<h3>For Students</h3>
<p>Store all your coursework, projects, and study materials in one secure place.</p>
</div>
</div>
</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>
</main>
{% endblock %}
{% block scripts %}
<script type="module" src="/static/js/components/pricing.js"></script>
{% endblock %}

View File

@ -1,381 +0,0 @@
{% extends "layouts/dashboard.html" %}
{% block title %}Manage Shares - Retoor's Cloud Solutions{% endblock %}
{% block dashboard_head %}
<style>
.shares-container {
padding: 1.5rem;
max-width: 1400px;
margin: 0 auto;
}
.shares-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.share-card {
background: white;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-left: 4px solid #0066cc;
transition: transform 0.2s, box-shadow 0.2s;
}
.share-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.share-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #eee;
}
.share-item-path {
font-weight: 600;
font-size: 1.1rem;
color: #333;
word-break: break-all;
flex: 1;
margin-right: 1rem;
}
.share-status {
padding: 0.4rem 0.8rem;
border-radius: 16px;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.share-status.active {
background: #d4edda;
color: #155724;
}
.share-status.inactive {
background: #f8d7da;
color: #721c24;
}
.share-status.expired {
background: #fff3cd;
color: #856404;
}
.share-card-body {
margin-bottom: 1rem;
}
.share-info-row {
display: flex;
justify-content: space-between;
padding: 0.5rem 0;
font-size: 0.9rem;
}
.share-info-label {
color: #666;
font-weight: 500;
}
.share-info-value {
color: #333;
font-weight: 600;
}
.share-link {
font-family: monospace;
font-size: 0.85rem;
color: #0066cc;
background: #f0f7ff;
padding: 0.5rem;
border-radius: 4px;
margin: 0.75rem 0;
display: block;
word-break: break-all;
}
.share-actions {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid #eee;
}
.share-actions button {
flex: 1;
min-width: 80px;
padding: 0.6rem 0.8rem;
font-size: 0.85rem;
}
.empty-state {
text-align: center;
padding: 4rem 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h3 {
margin-bottom: 0.5rem;
color: #333;
}
.empty-state p {
color: #666;
margin-bottom: 1.5rem;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
padding: 2rem;
border-radius: 8px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
}
.recipient-list {
margin-top: 1rem;
}
.recipient-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #f5f5f5;
margin-bottom: 0.5rem;
border-radius: 4px;
}
@media (max-width: 768px) {
.shares-grid {
grid-template-columns: 1fr;
}
}
</style>
{% endblock %}
{% block page_title %}Manage Shares{% endblock %}
{% block dashboard_content %}
<div class="shares-container">
{% if shares %}
<div class="shares-grid" id="shares-list">
{% for share in shares %}
<div class="share-card" data-share-id="{{ share.share_id }}">
<div class="share-card-header">
<div class="share-item-path">{{ share.item_path }}</div>
{% if not share.active %}
<span class="share-status inactive">Inactive</span>
{% elif share.expires_at and share.expires_at < now %}
<span class="share-status expired">Expired</span>
{% else %}
<span class="share-status active">Active</span>
{% endif %}
</div>
<div class="share-card-body">
<div class="share-link">/share/{{ share.share_id }}</div>
<div class="share-info-row">
<span class="share-info-label">Permission</span>
<span class="share-info-value">{{ share.permission }}</span>
</div>
<div class="share-info-row">
<span class="share-info-label">Scope</span>
<span class="share-info-value">{{ share.scope }}</span>
</div>
<div class="share-info-row">
<span class="share-info-label">Access Count</span>
<span class="share-info-value">{{ share.access_count or 0 }} views</span>
</div>
<div class="share-info-row">
<span class="share-info-label">Created</span>
<span class="share-info-value">{{ share.created_at[:10] }}</span>
</div>
<div class="share-info-row">
<span class="share-info-label">Expires</span>
<span class="share-info-value">{{ share.expires_at[:10] if share.expires_at else 'Never' }}</span>
</div>
</div>
<div class="share-actions">
<button class="btn-small copy-link-btn" data-share-id="{{ share.share_id }}">Copy Link</button>
<button class="btn-small view-details-btn" data-share-id="{{ share.share_id }}">Details</button>
{% if share.active %}
<button class="btn-small deactivate-btn" data-share-id="{{ share.share_id }}">Deactivate</button>
{% else %}
<button class="btn-small activate-btn" data-share-id="{{ share.share_id }}">Activate</button>
{% endif %}
<button class="btn-small delete-btn btn-danger" data-share-id="{{ share.share_id }}">Delete</button>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<div class="empty-state-icon">đź”—</div>
<h3>No Shares Yet</h3>
<p>You have not created any shares. Start sharing your files and folders with others.</p>
<a href="/files" class="btn-primary">Go to My Files</a>
</div>
{% endif %}
</div>
<div class="modal" id="share-details-modal">
<div class="modal-content">
<div class="modal-header">
<h2>Share Details</h2>
<button class="modal-close">&times;</button>
</div>
<div id="share-details-content">
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="module">
const sharesList = document.getElementById('shares-list');
const detailsModal = document.getElementById('share-details-modal');
const detailsContent = document.getElementById('share-details-content');
const modalClose = document.querySelector('.modal-close');
if (sharesList) {
sharesList.addEventListener('click', async (e) => {
const shareId = e.target.dataset.shareId;
if (!shareId) return;
if (e.target.classList.contains('view-details-btn')) {
await viewShareDetails(shareId);
} else if (e.target.classList.contains('copy-link-btn')) {
copyShareLink(shareId);
} else if (e.target.classList.contains('deactivate-btn')) {
await updateShare(shareId, 'deactivate');
} else if (e.target.classList.contains('activate-btn')) {
await updateShare(shareId, 'reactivate');
} else if (e.target.classList.contains('delete-btn')) {
if (confirm('Are you sure you want to delete this share?')) {
await updateShare(shareId, 'delete');
}
}
});
}
async function viewShareDetails(shareId) {
try {
const response = await fetch(`/api/sharing/${shareId}`);
const data = await response.json();
let html = `
<div class="form-group">
<label>Item Path</label>
<p>${data.share.item_path}</p>
</div>
<div class="form-group">
<label>Permission</label>
<p>${data.share.permission}</p>
</div>
<div class="form-group">
<label>Scope</label>
<p>${data.share.scope}</p>
</div>
`;
if (data.recipients && data.recipients.length > 0) {
html += `
<div class="form-group">
<label>Recipients</label>
<div class="recipient-list">
${data.recipients.map(r => `
<div class="recipient-item">
<div>
<div>${r.email}</div>
<small>Permission: ${r.permission}</small>
</div>
<div>
${r.accessed ? 'Accessed' : 'Not accessed'}
</div>
</div>
`).join('')}
</div>
</div>
`;
}
detailsContent.innerHTML = html;
detailsModal.classList.add('active');
} catch (error) {
alert('Error loading share details: ' + error.message);
}
}
function copyShareLink(shareId) {
const url = window.location.origin + '/share/' + shareId;
navigator.clipboard.writeText(url).then(() => {
alert('Share link copied to clipboard!');
});
}
async function updateShare(shareId, action) {
try {
const response = await fetch(`/api/sharing/${shareId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ action })
});
const result = await response.json();
if (result.success) {
location.reload();
} else {
alert('Error: ' + result.error);
}
} catch (error) {
alert('Error updating share: ' + error.message);
}
}
modalClose.addEventListener('click', () => {
detailsModal.classList.remove('active');
});
detailsModal.addEventListener('click', (e) => {
if (e.target === detailsModal) {
detailsModal.classList.remove('active');
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,81 @@
{% extends "layouts/base.html" %}
{% block title %}Pricing - Retoor's Cloud Solutions{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/components/pricing.css">
{% endblock %}
{% block content %}
<main>
<section class="pricing-hero">
<h1>Simple, Transparent Pricing</h1>
<p>Find the perfect plan for your needs.</p>
<div class="pricing-toggle">
<button class="btn-toggle active" data-period="monthly">Monthly</button>
<button class="btn-toggle" data-period="annually">Annually</button>
</div>
</section>
<section class="pricing-tiers">
<div class="pricing-card">
<h3>Free</h3>
<p class="price">$0<span>/month</span></p>
<ul>
<li>1 GB Storage</li>
<li>Basic Sync & Share</li>
<li>Standard Support</li>
</ul>
<a href="/register" class="btn-primary">Get Started</a>
</div>
<div class="pricing-card featured">
<h3>Personal</h3>
<p class="price">$9<span>/month</span></p>
<ul>
<li>100 GB Storage</li>
<li>Advanced Sync & Share</li>
<li>Priority Support</li>
<li>Version History</li>
</ul>
<a href="/register" class="btn-primary">Sign Up Now</a>
</div>
<div class="pricing-card">
<h3>Professional</h3>
<p class="price">$29<span>/month</span></p>
<ul>
<li>1 TB Storage</li>
<li>Team Collaboration</li>
<li>24/7 Premium Support</li>
<li>Advanced Security</li>
</ul>
<a href="/register" class="btn-primary">Sign Up Now</a>
</div>
<div class="pricing-card">
<h3>Business</h3>
<p class="price">$99<span>/month</span></p>
<ul>
<li>Unlimited Storage</li>
<li>Custom Solutions</li>
<li>Dedicated Account Manager</li>
<li>Advanced Analytics</li>
</ul>
<a href="/support" class="btn-primary">Contact Sales</a>
</div>
</section>
<section class="pricing-faq">
<h2>Frequently Asked Questions</h2>
<div class="faq-item">
<h3>Can I change my plan later?</h3>
<p>Yes, you can upgrade or downgrade your plan at any time from your dashboard.</p>
</div>
<div class="faq-item">
<h3>What payment methods do you accept?</h3>
<p>We accept all major credit cards, PayPal, and bank transfers for annual plans.</p>
</div>
</section>
</main>
{% endblock %}

View File

@ -52,14 +52,8 @@
<img src="/static/images/icon-families.svg" alt="Folder Icon" class="file-icon">
<a href="/files?path={{ item.path }}">{{ item.name }}</a>
{% else %}
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
{% 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 }}
{% endif %}
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
{{ item.name }}
{% endif %}
</td>
<td>{{ user.email }}</td>
@ -125,4 +119,4 @@
</div>
<script type="module" src="/static/js/main.js"></script>
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,62 @@
{% extends "layouts/base.html" %}
{% block title %}Security - Retoor's Cloud Solutions{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/components/security.css">
{% endblock %}
{% block content %}
<main>
<section class="security-hero">
<h1>Your Data, Our Priority. Uncompromising Security.</h1>
<p>At Retoor's Cloud Solutions, we understand the critical importance of data security and privacy. We employ industry-leading measures to ensure your information is always protected.</p>
</section>
<section class="security-pillars">
<div class="pillar-card">
<img src="/static/images/icon-professionals.svg" alt="Encryption Icon" class="icon">
<h3>Robust Encryption</h3>
<p>All your data is encrypted both in transit (TLS 1.2+) and at rest (AES-256), ensuring maximum confidentiality and integrity.</p>
</div>
<div class="pillar-card">
<img src="/static/images/icon-professionals.svg" alt="Access Control Icon" class="icon">
<h3>Advanced Access Control</h3>
<p>Implement granular permissions, multi-factor authentication (MFA), and strict access policies to keep your data safe from unauthorized access.</p>
</div>
<div class="pillar-card">
<img src="/static/images/icon-families.svg" alt="Privacy Icon" class="icon">
<h3>Unwavering Privacy</h3>
<p>We are committed to your privacy. Our policies are transparent, and we comply with global data protection regulations like GDPR and CCPA.</p>
</div>
<div class="pillar-card">
<img src="/static/images/icon-professionals.svg" alt="Infrastructure Icon" class="icon">
<h3>Secure Infrastructure</h3>
<p>Our data centers are physically secured, and our network is protected by advanced firewalls and intrusion detection systems, regularly audited for vulnerabilities.</p>
</div>
</section>
<section class="security-certifications">
<h2>Trust & Compliance</h2>
<div class="cert-grid">
<div class="cert-item">
<img src="/static/images/icon-professionals.svg" alt="ISO 27001 Certified" class="cert-logo">
<p>ISO 27001 Certified</p>
</div>
<div class="cert-item">
<img src="/static/images/icon-professionals.svg" alt="SOC 2 Compliant" class="cert-logo">
<p>SOC 2 Compliant</p>
</div>
<div class="cert-item">
<img src="/static/images/icon-families.svg" alt="GDPR Compliant" class="cert-logo">
<p>GDPR Compliant</p>
</div>
</div>
</section>
<section class="security-cta">
<h2>Have More Questions About Security?</h2>
<a href="/support" class="btn-primary">Contact Support</a>
</section>
</main>
{% endblock %}

View File

@ -1,34 +0,0 @@
{% extends "layouts/base.html" %}
{% block title %}Share Error{% endblock %}
{% block head %}
<style>
.error-container {
max-width: 600px;
margin: 4rem auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
text-align: center;
}
.error-icon {
font-size: 4rem;
margin-bottom: 1rem;
}
.error-message {
color: #666;
margin: 1.5rem 0;
}
</style>
{% endblock %}
{% block content %}
<div class="error-container">
<div class="error-icon">đź”’</div>
<h1>Access Denied</h1>
<p class="error-message">{{ error }}</p>
<a href="/" class="btn-primary">Go to Homepage</a>
</div>
{% endblock %}

View File

@ -1,81 +0,0 @@
{% extends "layouts/base.html" %}
{% block title %}Shared File - {{ file_name }}{% endblock %}
{% block head %}
<style>
.share-container {
max-width: 800px;
margin: 4rem auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.share-header {
border-bottom: 1px solid #eee;
padding-bottom: 1.5rem;
margin-bottom: 1.5rem;
}
.file-icon-large {
width: 64px;
height: 64px;
margin: 0 auto 1rem;
display: block;
}
.file-info {
display: grid;
grid-template-columns: 150px 1fr;
gap: 1rem;
margin: 1.5rem 0;
}
.file-info dt {
font-weight: 600;
}
.file-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
.permission-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
background: #e3f2fd;
color: #1976d2;
border-radius: 12px;
font-size: 0.875rem;
}
</style>
{% endblock %}
{% block content %}
<div class="share-container">
<div class="share-header">
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon-large">
<h1>{{ file_name }}</h1>
<span class="permission-badge">{{ permission }}</span>
</div>
<dl class="file-info">
<dt>File Name</dt>
<dd>{{ file_name }}</dd>
<dt>Size</dt>
<dd>{{ (file_size / 1024 / 1024)|round(2) }} MB</dd>
<dt>Path</dt>
<dd>{{ item_path }}</dd>
<dt>Permission</dt>
<dd>{{ permission }}</dd>
</dl>
<div class="file-actions">
{% if not disable_download %}
<a href="/share/{{ share_id }}/download" class="btn-primary" download>Download File</a>
{% else %}
<button class="btn-outline" disabled>Download Disabled</button>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,114 +0,0 @@
{% extends "layouts/base.html" %}
{% block title %}Shared Folder - {{ item_path }}{% endblock %}
{% block head %}
<style>
.share-container {
max-width: 1200px;
margin: 4rem auto;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.share-header {
border-bottom: 1px solid #eee;
padding-bottom: 1.5rem;
margin-bottom: 1.5rem;
}
.permission-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
background: #e3f2fd;
color: #1976d2;
border-radius: 12px;
font-size: 0.875rem;
margin-left: 1rem;
}
.file-list-table {
width: 100%;
}
.file-list-table table {
width: 100%;
border-collapse: collapse;
}
.file-list-table th {
background: #f5f5f5;
padding: 1rem;
text-align: left;
font-weight: 600;
}
.file-list-table td {
padding: 1rem;
border-top: 1px solid #eee;
}
.file-icon {
width: 20px;
height: 20px;
margin-right: 0.5rem;
vertical-align: middle;
}
</style>
{% endblock %}
{% block content %}
<div class="share-container">
<div class="share-header">
<h1>
{{ item_path.split('/')[-1] if item_path else 'Shared Folder' }}
<span class="permission-badge">{{ permission }}</span>
</h1>
<p>{{ item_path }}</p>
</div>
<div class="file-list-table">
<table>
<thead>
<tr>
<th>Name</th>
<th>Last Modified</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if files %}
{% for item in files %}
<tr>
<td>
{% if item.is_dir %}
<img src="/static/images/icon-families.svg" alt="Folder Icon" class="file-icon">
{{ item.name }}
{% else %}
<img src="/static/images/icon-professionals.svg" alt="File Icon" class="file-icon">
{{ item.name }}
{% endif %}
</td>
<td>{{ item.last_modified[:10] if item.last_modified else '' }}</td>
<td>
{% if item.is_dir %}
--
{% else %}
{{ (item.size / 1024 / 1024)|round(2) }} MB
{% endif %}
</td>
<td>
{% if not item.is_dir and not disable_download %}
<a href="/share/{{ share_id }}/download?file_path={{ item.path }}" class="btn-small">Download</a>
{% endif %}
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="4" style="text-align: center; padding: 2rem; color: #999;">
This folder is empty
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@ -13,7 +13,7 @@
<button class="btn-outline" id="upload-btn">Upload</button>
<button class="btn-outline" id="download-selected-btn" disabled>⬇️</button>
<button class="btn-outline" id="share-selected-btn" disabled>đź”—</button>
<button class="btn-outline" id="delete-selected-btn" disabled>🗑️</button>
<button class="btn-outline" id="delete-selected-btn" disabled>Delete</button>
{% endblock %}
{% block dashboard_content %}
@ -73,7 +73,7 @@
<button class="btn-small download-file-btn" data-path="{{ item.path }}">⬇️</button>
{% endif %}
<button class="btn-small share-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">đź”—</button>
<button class="btn-small btn-danger delete-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">🗑️</button>
<button class="btn-small btn-danger delete-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">Delete</button>
</div>
</td>
</tr>
@ -144,7 +144,7 @@
<p id="delete-message"></p>
<form id="delete-form" method="post">
<div class="modal-actions">
<button type="submit" class="btn-danger">🗑️</button>
<button type="submit" class="btn-danger">Delete</button>
<button type="button" class="btn-outline" onclick="closeModal('delete-modal')">Cancel</button>
</div>
</form>

View File

@ -0,0 +1,75 @@
{% extends "layouts/base.html" %}
{% block title %}Solutions - Retoor's Cloud Solutions{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/css/components/solutions.css">
{% endblock %}
{% block content %}
<main>
<section class="solutions-hero">
<h1>Powerful Cloud Solutions for Modern Needs</h1>
<p>Discover how Retoor's Cloud Solutions can empower your family, business, or academic pursuits with secure, accessible, and collaborative tools.</p>
</section>
<section class="solution-sections">
<div class="solution-card">
<img src="/static/images/icon-families.svg" alt="Family Storage Icon" class="icon">
<h2>Secure Family Storage</h2>
<p>Keep your family's precious memories safe and accessible. Securely store photos, videos, and important documents, and easily share them with loved ones.</p>
<ul>
<li>Private photo and video galleries</li>
<li>Shared family albums</li>
<li>Secure document vault for wills, deeds, etc.</li>
<li>Easy sharing with granular permissions</li>
</ul>
<a href="/pricing" class="btn-primary">See Family Plans</a>
</div>
<div class="solution-card">
<img src="/static/images/icon-professionals.svg" alt="Business Collaboration Icon" class="icon">
<h2>Business Collaboration & Productivity</h2>
<p>Boost your team's efficiency with seamless collaboration tools. Share files, co-edit documents, and manage projects from anywhere, securely.</p>
<ul>
<li>Real-time document co-editing</li>
<li>Secure file sharing with external partners</li>
<li>Version control and recovery</li>
<li>Team workspaces and project folders</li>
</ul>
<a href="/pricing" class="btn-primary">Explore Business Solutions</a>
</div>
<div class="solution-card">
<img src="/static/images/icon-students.svg" alt="Academic Storage Icon" class="icon">
<h2>Academic & Student Resources</h2>
<p>Organize your academic life with dedicated storage for projects, research, and notes. Access your study materials across all your devices.</p>
<ul>
<li>Centralized storage for assignments and research</li>
<li>Easy access from campus or home</li>
<li>Secure sharing for group projects</li>
<li>Integration with academic tools (coming soon)</li>
</ul>
<a href="/pricing" class="btn-primary">View Student Plans</a>
</div>
<div class="solution-card">
<img src="/static/images/icon-families.svg" alt="Automated Backup & Recovery Icon" class="icon">
<h2>Automated Backup & Recovery</h2>
<p>Never lose a file again. Our automated backup solutions ensure your data is always safe, with easy recovery options for any scenario.</p>
<ul>
<li>Scheduled automatic backups</li>
<li>Point-in-time recovery</li>
<li>Disaster recovery planning</li>
<li>Secure, off-site data replication</li>
</ul>
<a href="/pricing" class="btn-primary">Learn About Backup</a>
</div>
</section>
<section class="solutions-cta">
<h2>Ready to Explore Our Plans?</h2>
<a href="/pricing" class="btn-primary">View All Pricing</a>
</section>
</main>
{% endblock %}

View File

@ -10,7 +10,7 @@
{% block dashboard_actions %}
<button class="btn-outline" id="restore-selected-btn" disabled>Restore</button>
<button class="btn-outline" id="delete-selected-btn" disabled>🗑️</button>
<button class="btn-outline" id="delete-selected-btn" disabled>Delete Permanently</button>
{% endblock %}
{% block dashboard_content %}
@ -67,7 +67,7 @@
<td>
<div class="action-buttons">
<button class="btn-small restore-file-btn" data-path="{{ item.path }}">Restore</button>
<button class="btn-small btn-danger delete-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">🗑️</button>
<button class="btn-small btn-danger delete-file-btn" data-path="{{ item.path }}" data-name="{{ item.name }}">Delete Permanently</button>
</div>
</td>
</tr>
@ -104,7 +104,7 @@
<p id="delete-message"></p>
<form id="delete-form" method="post">
<div class="modal-actions">
<button type="submit" class="btn-danger">🗑️</button>
<button type="submit" class="btn-danger">Delete Permanently</button>
<button type="button" class="btn-outline" onclick="closeModal('delete-modal')">Cancel</button>
</div>
</form>

View File

@ -44,7 +44,7 @@
<div style="display: flex; gap: 10px; margin-top: 30px;">
<a href="/users/{{ user_data.email }}/edit" class="btn-primary">Edit Quota</a>
<form action="/users/{{ user_data.email }}/delete" method="post" style="display: inline;" onsubmit="return confirm('Are you sure you want to delete this user?');">
<button type="submit" class="btn-danger">🗑️</button>
<button type="submit" class="btn-danger">Delete User</button>
</form>
<a href="/users" class="btn-outline">Back to Users</a>
</div>

View File

@ -1,381 +0,0 @@
import aiohttp_jinja2
from aiohttp import web
import json
import logging
from ..helpers.email_sender import send_email
from ..helpers.auth import login_required
logger = logging.getLogger(__name__)
async def send_share_invitations(app, sender_email, recipient_emails, item_path, share_url, permission, password):
for recipient in recipient_emails:
subject = f"{sender_email} shared '{item_path}' with you"
password_note = ""
if password:
password_note = f"<p><strong>This share is password protected.</strong> You will need to enter the password to access it.</p>"
body = f"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<h2>You have been invited to access a shared item</h2>
<p><strong>{sender_email}</strong> has shared <strong>{item_path}</strong> with you.</p>
<div style="background: #f5f5f5; padding: 15px; border-radius: 4px; margin: 20px 0;">
<p><strong>Permission Level:</strong> {permission}</p>
<p><strong>Item:</strong> {item_path}</p>
</div>
{password_note}
<p>Click the button below to access the shared item:</p>
<a href="{share_url}" style="display: inline-block; background: #0066cc; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; margin: 20px 0;">
Access Shared Item
</a>
<p>Or copy and paste this link into your browser:</p>
<p style="background: #f5f5f5; padding: 10px; border-radius: 4px; word-break: break-all;">
{share_url}
</p>
<hr style="margin: 30px 0; border: none; border-top: 1px solid #ddd;">
<p style="color: #666; font-size: 12px;">
This invitation was sent by Retoor's Cloud Solutions.<br>
If you believe you received this email in error, please disregard it.
</p>
</body>
</html>
"""
try:
await send_email(app, recipient, subject, body)
logger.info(f"Sent share invitation to {recipient} for {item_path}")
except Exception as e:
logger.error(f"Failed to send share invitation to {recipient}: {e}")
@login_required
@aiohttp_jinja2.template('pages/create_share.html')
async def create_share_page(request: web.Request):
user = request['user']
user_email = user['email']
item_path = request.query.get('item_path', '')
return {
'request': request,
'user_email': user_email,
'item_path': item_path,
'active_page': 'my_shares',
'user': user
}
@login_required
async def create_share_handler(request: web.Request):
user = request['user']
user_email = user['email']
try:
data = await request.json()
except json.JSONDecodeError:
return web.json_response({'error': 'Invalid JSON'}, status=400)
item_path = data.get('item_path')
if not item_path:
return web.json_response({'error': 'item_path is required'}, status=400)
permission = data.get('permission', 'view')
scope = data.get('scope', 'public')
password = data.get('password')
expiration_days = data.get('expiration_days')
disable_download = data.get('disable_download', False)
recipient_emails = data.get('recipient_emails', [])
file_service = request.app['file_service']
try:
share_id = await file_service.generate_share_link(
user_email=user_email,
item_path=item_path,
permission=permission,
scope=scope,
password=password,
expiration_days=expiration_days,
disable_download=disable_download,
recipient_emails=recipient_emails
)
if share_id:
share_url = str(request.url.origin()) + f'/share/{share_id}'
if recipient_emails and len(recipient_emails) > 0:
logger.info(f"Sending email invitations to {len(recipient_emails)} recipients: {recipient_emails}")
await send_share_invitations(
request.app,
user_email,
recipient_emails,
item_path,
share_url,
permission,
password
)
else:
logger.debug(f"No recipients specified for share {share_id}, skipping email invitations")
return web.json_response({
'success': True,
'share_id': share_id,
'share_url': share_url
})
else:
return web.json_response({'error': 'Failed to create share'}, status=400)
except Exception as e:
logger.error(f"Error creating share: {e}")
return web.json_response({'error': str(e)}, status=500)
async def view_share(request: web.Request):
share_id = request.match_info['share_id']
file_service = request.app['file_service']
password = request.query.get('password')
accessor_email = request.get('user', {}).get('email') if request.get('user') else None
shared_item = await file_service.get_shared_item(share_id, password, accessor_email)
share = await file_service.sharing_service.get_share(share_id)
if not share:
# Check for specific reasons why the share might not be found
# (e.g., expired, deactivated, or truly not found)
# This requires deeper inspection within sharing_service or replicating its logic,
# for now, a generic "not found" is sufficient if get_share returns None.
return aiohttp_jinja2.render_template('pages/share_error.html', request, {
'request': request,
'error': 'Share link is invalid, expired, or deactivated.'
})
# Now verify access with the retrieved share object
if not await file_service.sharing_service.verify_share_access(share_id, password, accessor_email):
# Determine more specific access denial reasons
error_message = 'Access to this share is denied.'
# Check for password requirement
if share.get("password_hash") and not password:
error_message = 'This share is password protected. Please provide the correct password.'
elif share.get("password_hash") and password and file_service.sharing_service._hash_password(password) != share["password_hash"]:
error_message = 'Incorrect password for this share.'
# Check for private scope and recipient
elif share["scope"] == file_service.sharing_service.SCOPE_PRIVATE and accessor_email:
recipients = await file_service.sharing_service._load_share_recipients(share_id)
if accessor_email not in recipients:
error_message = 'You are not authorized to access this private share.'
# Check for account-based scope and anonymous access
elif share["scope"] == file_service.sharing_service.SCOPE_ACCOUNT_BASED and not accessor_email:
error_message = 'This share requires you to be logged in to an authorized account.'
return aiohttp_jinja2.render_template('pages/share_error.html', request, {
'request': request,
'error': error_message
})
# If access is verified, record it
await file_service.sharing_service.record_share_access(share_id, accessor_email)
# Proceed with getting the shared item details
shared_item = share # Use the already fetched share object
metadata = await file_service._load_metadata(shared_item['owner_email'])
item_meta = metadata.get(shared_item['item_path'])
if not item_meta:
return aiohttp_jinja2.render_template('pages/share_error.html', request, {
'request': request,
'error': 'The shared item could not be found or has been removed.'
})
is_folder = item_meta.get('type') == 'dir'
if is_folder:
files = await file_service.get_shared_folder_content(share_id, password, accessor_email)
return aiohttp_jinja2.render_template('pages/share_folder.html', request, {
'request': request,
'share_id': share_id,
'item_path': shared_item['item_path'],
'files': files,
'permission': shared_item.get('permission', 'view'),
'disable_download': shared_item.get('disable_download', False)
})
else:
return aiohttp_jinja2.render_template('pages/share_file.html', request, {
'request': request,
'share_id': share_id,
'item_path': shared_item['item_path'],
'file_name': item_meta.get('name', shared_item['item_path'].split('/')[-1]),
'file_size': item_meta.get('size', 0),
'permission': shared_item.get('permission', 'view'),
'disable_download': shared_item.get('disable_download', False)
})
async def download_shared_file(request: web.Request):
share_id = request.match_info['share_id']
file_service = request.app['file_service']
password = request.query.get('password')
accessor_email = request.get('user', {}).get('email') if request.get('user') else None
requested_file_path = request.query.get('file_path')
# First, verify access to the share itself
share = await file_service.sharing_service.get_share(share_id)
if not share:
return web.Response(text='Share link is invalid, expired, or deactivated.', status=404)
if not await file_service.sharing_service.verify_share_access(share_id, password, accessor_email):
error_message = 'Access to this share is denied.'
if share.get("password_hash") and not password:
error_message = 'This share is password protected. Please provide the correct password.'
elif share.get("password_hash") and password and file_service.sharing_service._hash_password(password) != share["password_hash"]:
error_message = 'Incorrect password for this share.'
elif share["scope"] == file_service.sharing_service.SCOPE_PRIVATE and accessor_email:
recipients = await file_service.sharing_service._load_share_recipients(share_id)
if accessor_email not in recipients:
error_message = 'You are not authorized to access this private share.'
elif share["scope"] == file_service.sharing_service.SCOPE_ACCOUNT_BASED and not accessor_email:
error_message = 'This share requires you to be logged in to an authorized account.'
return web.Response(text=error_message, status=403) # Use 403 Forbidden for access denied
# If access is verified, record it
await file_service.sharing_service.record_share_access(share_id, accessor_email)
# Then attempt to get the file content
result = await file_service.get_shared_file_content(share_id, password, accessor_email, requested_file_path)
if not result:
# This means the file itself was not found or download was disabled
if share.get("disable_download", False):
return web.Response(text='Download is disabled for this share.', status=403)
return web.Response(text='The requested file could not be found or has been removed.', status=404)
content, filename = result
return web.Response(
body=content,
headers={
'Content-Type': 'application/octet-stream',
'Content-Disposition': f'attachment; filename="{filename}"'
}
)
@login_required
@aiohttp_jinja2.template('pages/manage_shares.html')
async def manage_shares(request: web.Request):
import datetime
user = request['user']
user_email = user['email']
file_service = request.app['file_service']
sharing_service = file_service.sharing_service
shares = await sharing_service.list_user_shares(user_email)
return {
'request': request,
'user_email': user_email,
'shares': shares,
'active_page': 'my_shares',
'user': user,
'now': datetime.datetime.now(datetime.timezone.utc).isoformat()
}
@login_required
async def get_share_details(request: web.Request):
user = request['user']
user_email = user['email']
share_id = request.match_info['share_id']
file_service = request.app['file_service']
sharing_service = file_service.sharing_service
share = await sharing_service.get_share(share_id)
if not share or share['owner_email'] != user_email:
return web.json_response({'error': 'Share not found'}, status=404)
recipients = await sharing_service.get_share_recipients(share_id)
return web.json_response({
'share': share,
'recipients': list(recipients.values())
})
@login_required
async def update_share(request: web.Request):
user = request['user']
user_email = user['email']
share_id = request.match_info['share_id']
try:
data = await request.json()
except json.JSONDecodeError:
return web.json_response({'error': 'Invalid JSON'}, status=400)
file_service = request.app['file_service']
sharing_service = file_service.sharing_service
action = data.get('action')
if action == 'update_permission':
permission = data.get('permission')
success = await sharing_service.update_share_permission(user_email, share_id, permission)
elif action == 'update_expiration':
expiration_days = data.get('expiration_days')
success = await sharing_service.update_share_expiration(user_email, share_id, expiration_days)
elif action == 'deactivate':
success = await sharing_service.deactivate_share(user_email, share_id)
elif action == 'reactivate':
success = await sharing_service.reactivate_share(user_email, share_id)
elif action == 'delete':
success = await sharing_service.delete_share(user_email, share_id)
elif action == 'add_recipient':
recipient_email = data.get('recipient_email')
permission = data.get('permission', 'view')
success = await sharing_service.add_share_recipient(user_email, share_id, recipient_email, permission)
elif action == 'remove_recipient':
recipient_email = data.get('recipient_email')
success = await sharing_service.remove_share_recipient(user_email, share_id, recipient_email)
elif action == 'update_recipient_permission':
recipient_email = data.get('recipient_email')
permission = data.get('permission')
success = await sharing_service.update_recipient_permission(user_email, share_id, recipient_email, permission)
else:
return web.json_response({'error': 'Invalid action'}, status=400)
if success:
return web.json_response({'success': True})
else:
return web.json_response({'error': 'Operation failed'}, status=400)
@login_required
async def get_item_shares(request: web.Request):
user = request['user']
user_email = user['email']
item_path = request.query.get('item_path')
if not item_path:
return web.json_response({'error': 'item_path is required'}, status=400)
file_service = request.app['file_service']
sharing_service = file_service.sharing_service
shares = await sharing_service.get_shares_for_item(user_email, item_path)
return web.json_response({'shares': shares})

View File

@ -142,22 +142,8 @@ class SiteView(web.View):
@login_required
async def recent(self):
user_email = self.request["user"]["email"]
file_service = self.request.app["file_service"]
recent_files = await file_service.get_recent_files(user_email)
# 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 recent_files:
if not item['is_dir']:
from pathlib import Path
ext = Path(item['name']).suffix.lower()
item['is_editable'] = ext in editable_extensions
item['is_viewable'] = ext in viewable_extensions
return aiohttp_jinja2.render_template(
"pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "recent", "recent_files": recent_files}
"pages/recent.html", self.request, {"request": self.request, "errors": {}, "user": self.request["user"], "active_page": "recent"}
)
@login_required
@ -200,13 +186,11 @@ class FileBrowserView(web.View):
# 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'}
user_service = self.request.app["user_service"]
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
item['is_favorite'] = await user_service.is_favorite(user_email, item['path'])
success_message = self.request.query.get("success")
error_message = self.request.query.get("error")
@ -364,19 +348,6 @@ class FileBrowserView(web.View):
logger.error(f"FileBrowserView: Failed to generate any share links for user {user_email}")
return json_response({"error": "Failed to generate share links for any selected items"}, status=500)
elif route_name == "toggle_favorite":
data = await self.request.json()
file_path = data.get("file_path")
if not file_path:
return json_response({"error": "File path is required"}, status=400)
user_service = self.request.app["user_service"]
is_fav = await user_service.is_favorite(user_email, file_path)
if is_fav:
await user_service.remove_favorite(user_email, file_path)
else:
await user_service.add_favorite(user_email, file_path)
return json_response({"is_favorite": not is_fav})
logger.warning(f"FileBrowserView: Unknown file action for POST request: {route_name}")
raise web.HTTPBadRequest(text="Unknown file action")