2025-11-09 23:29:07 +01:00
|
|
|
import argparse
|
|
|
|
|
import uvicorn
|
2025-11-10 15:46:40 +01:00
|
|
|
import logging
|
|
|
|
|
from contextlib import asynccontextmanager
|
2025-11-14 01:59:01 +01:00
|
|
|
from fastapi import FastAPI, Request, HTTPException, status
|
2025-11-09 23:29:07 +01:00
|
|
|
from fastapi.staticfiles import StaticFiles
|
2025-11-16 01:48:39 +01:00
|
|
|
from fastapi.responses import HTMLResponse, JSONResponse, Response, RedirectResponse
|
|
|
|
|
from fastapi.templating import Jinja2Templates
|
2025-11-09 23:29:07 +01:00
|
|
|
from tortoise.contrib.fastapi import register_tortoise
|
|
|
|
|
from .settings import settings
|
2025-11-13 23:22:05 +01:00
|
|
|
from .routers import (
|
|
|
|
|
auth,
|
|
|
|
|
users,
|
|
|
|
|
folders,
|
|
|
|
|
files,
|
|
|
|
|
shares,
|
|
|
|
|
search,
|
|
|
|
|
admin,
|
|
|
|
|
starred,
|
|
|
|
|
billing,
|
|
|
|
|
admin_billing,
|
feat: add admin panel with session auth, user management, and billing dashboard
Implement a full-featured admin backend at `/manage/` with login authentication using
signed cookies and constant-time credential verification. The panel includes a dashboard
showing total/active users, storage usage, monthly revenue, and pending invoices, plus
user management, payment overview, and pricing configuration pages. New environment
variables `ADMIN_USERNAME`, `ADMIN_PASSWORD`, `ADMIN_SESSION_SECRET`, and
`ADMIN_SESSION_EXPIRE_HOURS` control access. The admin router is registered in main.py
and all new templates, auth module, and router files are added.
2026-01-05 21:50:41 +01:00
|
|
|
manage,
|
2025-11-13 23:22:05 +01:00
|
|
|
)
|
2025-11-09 23:29:07 +01:00
|
|
|
from . import webdav
|
2025-11-10 15:46:40 +01:00
|
|
|
from .schemas import ErrorResponse
|
2025-11-29 11:17:47 +01:00
|
|
|
from .middleware import UsageTrackingMiddleware, RateLimitMiddleware, SecurityHeadersMiddleware
|
|
|
|
|
from .monitoring import health_router
|
feat: add 2fa support, starred items, batch operations, and logging infrastructure
- Implement two-factor authentication with TOTP verification, recovery codes, and QR code setup in auth flow
- Add is_starred and last_accessed_at fields to File and Folder models for bookmarking and recency tracking
- Introduce batch operation endpoints for files and folders (delete, star, unstar, move, copy)
- Configure structured logging with timestamps and module names, replacing print statements
- Register admin and starred routers, add global HTTPException handler with JSON error responses
- Update authenticate_user and create_access_token to handle 2FA verification and token claims
2025-11-10 01:58:41 +01:00
|
|
|
|
2025-11-13 23:22:05 +01:00
|
|
|
logging.basicConfig(
|
|
|
|
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
|
|
|
)
|
feat: add 2fa support, starred items, batch operations, and logging infrastructure
- Implement two-factor authentication with TOTP verification, recovery codes, and QR code setup in auth flow
- Add is_starred and last_accessed_at fields to File and Folder models for bookmarking and recency tracking
- Introduce batch operation endpoints for files and folders (delete, star, unstar, move, copy)
- Configure structured logging with timestamps and module names, replacing print statements
- Register admin and starred routers, add global HTTPException handler with JSON error responses
- Update authenticate_user and create_access_token to handle 2FA verification and token claims
2025-11-10 01:58:41 +01:00
|
|
|
logger = logging.getLogger(__name__)
|
2025-11-09 23:29:07 +01:00
|
|
|
|
2025-11-29 11:17:47 +01:00
|
|
|
enterprise_components = {
|
|
|
|
|
"db_manager": None,
|
|
|
|
|
"cache": None,
|
|
|
|
|
"lock_manager": None,
|
|
|
|
|
"token_manager": None,
|
|
|
|
|
"rate_limiter": None,
|
|
|
|
|
"webdav_locks": None,
|
|
|
|
|
"task_queue": None,
|
2025-11-29 12:18:53 +01:00
|
|
|
"dal": None,
|
|
|
|
|
"write_buffer": None,
|
2025-11-29 11:17:47 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def init_enterprise_components():
|
|
|
|
|
from .database.manager import init_db_manager
|
|
|
|
|
from .cache.layer import init_cache
|
|
|
|
|
from .concurrency.locks import init_lock_manager
|
|
|
|
|
from .concurrency.webdav_locks import init_webdav_locks
|
|
|
|
|
from .concurrency.atomic import init_atomic_ops
|
|
|
|
|
from .auth_tokens import init_token_manager
|
|
|
|
|
from .middleware.rate_limit import init_rate_limiter
|
|
|
|
|
from .workers.queue import init_task_queue
|
2025-11-29 12:18:53 +01:00
|
|
|
from .enterprise.dal import init_dal
|
|
|
|
|
from .enterprise.write_buffer import init_write_buffer, WriteType
|
2025-11-29 11:17:47 +01:00
|
|
|
|
|
|
|
|
enterprise_components["db_manager"] = await init_db_manager("data")
|
|
|
|
|
logger.info("Enterprise: Per-user database manager initialized")
|
|
|
|
|
|
|
|
|
|
enterprise_components["cache"] = await init_cache(maxsize=10000, flush_interval=30)
|
|
|
|
|
logger.info("Enterprise: Cache layer initialized")
|
|
|
|
|
|
2025-11-29 12:18:53 +01:00
|
|
|
enterprise_components["dal"] = await init_dal(cache_size=50000)
|
|
|
|
|
logger.info("Enterprise: Data Access Layer initialized")
|
|
|
|
|
|
|
|
|
|
enterprise_components["write_buffer"] = await init_write_buffer(
|
|
|
|
|
flush_interval=60.0,
|
|
|
|
|
max_buffer_size=1000,
|
|
|
|
|
)
|
|
|
|
|
from .activity import _flush_activities
|
|
|
|
|
from .billing.usage_tracker import _flush_usage_records
|
|
|
|
|
enterprise_components["write_buffer"].register_handler(WriteType.ACTIVITY, _flush_activities)
|
|
|
|
|
enterprise_components["write_buffer"].register_handler(WriteType.USAGE_RECORD, _flush_usage_records)
|
|
|
|
|
logger.info("Enterprise: Write buffer initialized (60s flush interval)")
|
|
|
|
|
|
2025-11-29 11:17:47 +01:00
|
|
|
enterprise_components["lock_manager"] = await init_lock_manager(default_timeout=30.0)
|
|
|
|
|
logger.info("Enterprise: Lock manager initialized")
|
|
|
|
|
|
|
|
|
|
init_atomic_ops()
|
|
|
|
|
logger.info("Enterprise: Atomic operations initialized")
|
|
|
|
|
|
|
|
|
|
enterprise_components["webdav_locks"] = await init_webdav_locks(
|
|
|
|
|
enterprise_components["db_manager"]
|
|
|
|
|
)
|
|
|
|
|
logger.info("Enterprise: Persistent WebDAV locks initialized")
|
|
|
|
|
|
|
|
|
|
enterprise_components["token_manager"] = await init_token_manager(
|
|
|
|
|
enterprise_components["db_manager"]
|
|
|
|
|
)
|
|
|
|
|
logger.info("Enterprise: Token manager with revocation initialized")
|
|
|
|
|
|
|
|
|
|
enterprise_components["rate_limiter"] = await init_rate_limiter()
|
|
|
|
|
logger.info("Enterprise: Rate limiter initialized")
|
|
|
|
|
|
|
|
|
|
enterprise_components["task_queue"] = await init_task_queue(max_workers=4)
|
|
|
|
|
logger.info("Enterprise: Background task queue initialized")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def shutdown_enterprise_components():
|
|
|
|
|
from .database.manager import shutdown_db_manager
|
|
|
|
|
from .cache.layer import shutdown_cache
|
|
|
|
|
from .concurrency.locks import shutdown_lock_manager
|
|
|
|
|
from .concurrency.webdav_locks import shutdown_webdav_locks
|
|
|
|
|
from .auth_tokens import shutdown_token_manager
|
|
|
|
|
from .middleware.rate_limit import shutdown_rate_limiter
|
|
|
|
|
from .workers.queue import shutdown_task_queue
|
2025-11-29 12:18:53 +01:00
|
|
|
from .enterprise.dal import shutdown_dal
|
|
|
|
|
from .enterprise.write_buffer import shutdown_write_buffer
|
2025-11-29 11:17:47 +01:00
|
|
|
|
|
|
|
|
await shutdown_task_queue()
|
|
|
|
|
logger.info("Enterprise: Task queue stopped")
|
|
|
|
|
|
|
|
|
|
await shutdown_rate_limiter()
|
|
|
|
|
logger.info("Enterprise: Rate limiter stopped")
|
|
|
|
|
|
|
|
|
|
await shutdown_token_manager()
|
|
|
|
|
logger.info("Enterprise: Token manager stopped")
|
|
|
|
|
|
|
|
|
|
await shutdown_webdav_locks()
|
|
|
|
|
logger.info("Enterprise: WebDAV locks stopped")
|
|
|
|
|
|
|
|
|
|
await shutdown_lock_manager()
|
|
|
|
|
logger.info("Enterprise: Lock manager stopped")
|
|
|
|
|
|
2025-11-29 12:18:53 +01:00
|
|
|
await shutdown_write_buffer()
|
|
|
|
|
logger.info("Enterprise: Write buffer stopped (flushed pending writes)")
|
|
|
|
|
|
|
|
|
|
await shutdown_dal()
|
|
|
|
|
logger.info("Enterprise: Data Access Layer stopped")
|
|
|
|
|
|
2025-11-29 11:17:47 +01:00
|
|
|
await shutdown_cache()
|
|
|
|
|
logger.info("Enterprise: Cache layer stopped")
|
|
|
|
|
|
|
|
|
|
await shutdown_db_manager()
|
|
|
|
|
logger.info("Enterprise: Database manager stopped")
|
|
|
|
|
|
2025-11-13 23:22:05 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
logger.info("Starting up...")
|
2025-11-29 11:17:47 +01:00
|
|
|
|
|
|
|
|
await init_enterprise_components()
|
|
|
|
|
logger.info("All enterprise components initialized")
|
|
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
logger.info("Database connected.")
|
|
|
|
|
from .billing.scheduler import start_scheduler
|
|
|
|
|
from .billing.models import PricingConfig
|
2025-11-11 12:47:26 +01:00
|
|
|
from .mail import email_service
|
2025-11-13 23:22:05 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
start_scheduler()
|
|
|
|
|
logger.info("Billing scheduler started")
|
2025-11-11 12:47:26 +01:00
|
|
|
await email_service.start()
|
|
|
|
|
logger.info("Email service started")
|
2025-11-10 15:46:40 +01:00
|
|
|
pricing_count = await PricingConfig.all().count()
|
|
|
|
|
if pricing_count == 0:
|
|
|
|
|
from decimal import Decimal
|
2025-11-13 23:22:05 +01:00
|
|
|
|
|
|
|
|
await PricingConfig.create(
|
|
|
|
|
config_key="storage_per_gb_month",
|
|
|
|
|
config_value=Decimal("0.0045"),
|
|
|
|
|
description="Storage cost per GB per month",
|
|
|
|
|
unit="per_gb_month",
|
|
|
|
|
)
|
|
|
|
|
await PricingConfig.create(
|
|
|
|
|
config_key="bandwidth_egress_per_gb",
|
|
|
|
|
config_value=Decimal("0.009"),
|
|
|
|
|
description="Bandwidth egress cost per GB",
|
|
|
|
|
unit="per_gb",
|
|
|
|
|
)
|
|
|
|
|
await PricingConfig.create(
|
|
|
|
|
config_key="bandwidth_ingress_per_gb",
|
|
|
|
|
config_value=Decimal("0.0"),
|
|
|
|
|
description="Bandwidth ingress cost per GB (free)",
|
|
|
|
|
unit="per_gb",
|
|
|
|
|
)
|
|
|
|
|
await PricingConfig.create(
|
|
|
|
|
config_key="free_tier_storage_gb",
|
|
|
|
|
config_value=Decimal("15"),
|
|
|
|
|
description="Free tier storage in GB",
|
|
|
|
|
unit="gb",
|
|
|
|
|
)
|
|
|
|
|
await PricingConfig.create(
|
|
|
|
|
config_key="free_tier_bandwidth_gb",
|
|
|
|
|
config_value=Decimal("15"),
|
|
|
|
|
description="Free tier bandwidth in GB per month",
|
|
|
|
|
unit="gb",
|
|
|
|
|
)
|
|
|
|
|
await PricingConfig.create(
|
|
|
|
|
config_key="tax_rate_default",
|
|
|
|
|
config_value=Decimal("0.0"),
|
|
|
|
|
description="Default tax rate (0 = no tax)",
|
|
|
|
|
unit="percentage",
|
|
|
|
|
)
|
2025-11-10 15:46:40 +01:00
|
|
|
logger.info("Default pricing configuration initialized")
|
|
|
|
|
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
from .billing.scheduler import stop_scheduler
|
2025-11-13 23:22:05 +01:00
|
|
|
|
2025-11-10 15:46:40 +01:00
|
|
|
stop_scheduler()
|
|
|
|
|
logger.info("Billing scheduler stopped")
|
2025-11-11 12:47:26 +01:00
|
|
|
await email_service.stop()
|
|
|
|
|
logger.info("Email service stopped")
|
2025-11-29 11:17:47 +01:00
|
|
|
|
|
|
|
|
await shutdown_enterprise_components()
|
|
|
|
|
logger.info("All enterprise components shut down")
|
2025-11-10 15:46:40 +01:00
|
|
|
print("Shutting down...")
|
|
|
|
|
|
2025-11-13 23:22:05 +01:00
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
app = FastAPI(
|
chore: rebrand all user-facing strings from RBox to MyWebdav across docs, emails, and config
Replace every occurrence of "RBox" with "MyWebdav" in README.md, invoice templates, welcome emails, share notifications, FastAPI metadata, default database filename, domain name, S3 bucket name, TOTP issuer, and WebDAV realm header to reflect the commercial rebranding of the application.
2025-11-13 12:05:05 +01:00
|
|
|
title="MyWebdav Cloud Storage",
|
|
|
|
|
description="A commercial cloud storage web application",
|
2025-11-09 23:29:07 +01:00
|
|
|
version="0.1.0",
|
2025-11-13 23:22:05 +01:00
|
|
|
lifespan=lifespan,
|
2025-11-09 23:29:07 +01:00
|
|
|
)
|
|
|
|
|
|
2025-11-16 01:54:58 +01:00
|
|
|
templates = Jinja2Templates(directory="mywebdav/templates")
|
2025-11-16 01:48:39 +01:00
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
app.include_router(auth.router)
|
|
|
|
|
app.include_router(users.router)
|
|
|
|
|
app.include_router(folders.router)
|
|
|
|
|
app.include_router(files.router)
|
|
|
|
|
app.include_router(shares.router)
|
|
|
|
|
app.include_router(search.router)
|
2025-11-10 15:46:40 +01:00
|
|
|
app.include_router(admin.router)
|
|
|
|
|
app.include_router(starred.router)
|
|
|
|
|
app.include_router(billing.router)
|
|
|
|
|
app.include_router(admin_billing.router)
|
feat: add admin panel with session auth, user management, and billing dashboard
Implement a full-featured admin backend at `/manage/` with login authentication using
signed cookies and constant-time credential verification. The panel includes a dashboard
showing total/active users, storage usage, monthly revenue, and pending invoices, plus
user management, payment overview, and pricing configuration pages. New environment
variables `ADMIN_USERNAME`, `ADMIN_PASSWORD`, `ADMIN_SESSION_SECRET`, and
`ADMIN_SESSION_EXPIRE_HOURS` control access. The admin router is registered in main.py
and all new templates, auth module, and router files are added.
2026-01-05 21:50:41 +01:00
|
|
|
app.include_router(manage.router)
|
2025-11-09 23:29:07 +01:00
|
|
|
app.include_router(webdav.router)
|
2025-11-29 11:17:47 +01:00
|
|
|
app.include_router(health_router)
|
2025-11-09 23:29:07 +01:00
|
|
|
|
2025-11-29 11:17:47 +01:00
|
|
|
app.add_middleware(SecurityHeadersMiddleware, enable_hsts=False, enable_csp=True)
|
|
|
|
|
app.add_middleware(RateLimitMiddleware)
|
2025-11-10 15:46:40 +01:00
|
|
|
app.add_middleware(UsageTrackingMiddleware)
|
|
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static")
|
|
|
|
|
|
|
|
|
|
register_tortoise(
|
|
|
|
|
app,
|
|
|
|
|
db_url=settings.DATABASE_URL,
|
2025-11-13 23:22:05 +01:00
|
|
|
modules={"models": ["mywebdav.models"], "billing": ["mywebdav.billing.models"]},
|
2025-11-09 23:29:07 +01:00
|
|
|
generate_schemas=True,
|
|
|
|
|
add_exception_handlers=True,
|
|
|
|
|
)
|
|
|
|
|
|
2025-11-13 23:22:05 +01:00
|
|
|
|
feat: add 2fa support, starred items, batch operations, and logging infrastructure
- Implement two-factor authentication with TOTP verification, recovery codes, and QR code setup in auth flow
- Add is_starred and last_accessed_at fields to File and Folder models for bookmarking and recency tracking
- Introduce batch operation endpoints for files and folders (delete, star, unstar, move, copy)
- Configure structured logging with timestamps and module names, replacing print statements
- Register admin and starred routers, add global HTTPException handler with JSON error responses
- Update authenticate_user and create_access_token to handle 2FA verification and token claims
2025-11-10 01:58:41 +01:00
|
|
|
@app.exception_handler(HTTPException)
|
|
|
|
|
async def http_exception_handler(request: Request, exc: HTTPException):
|
2025-11-13 23:22:05 +01:00
|
|
|
logger.error(
|
|
|
|
|
f"HTTPException: {exc.status_code} - {exc.detail} for URL: {request.url}"
|
|
|
|
|
)
|
2025-11-14 01:59:01 +01:00
|
|
|
|
|
|
|
|
headers = exc.headers
|
|
|
|
|
|
|
|
|
|
# For WebDAV authentication challenges, we must return the headers
|
|
|
|
|
# from the exception and an empty body. A JSON body will confuse WebDAV clients.
|
|
|
|
|
if request.url.path.startswith("/webdav") and exc.status_code == status.HTTP_401_UNAUTHORIZED:
|
|
|
|
|
return Response(status_code=exc.status_code, headers=headers)
|
|
|
|
|
|
|
|
|
|
# For other WebDAV errors, it's better to return a text body than JSON
|
|
|
|
|
if request.url.path.startswith("/webdav"):
|
|
|
|
|
return Response(content=exc.detail, status_code=exc.status_code, headers=headers)
|
|
|
|
|
|
feat: add 2fa support, starred items, batch operations, and logging infrastructure
- Implement two-factor authentication with TOTP verification, recovery codes, and QR code setup in auth flow
- Add is_starred and last_accessed_at fields to File and Folder models for bookmarking and recency tracking
- Introduce batch operation endpoints for files and folders (delete, star, unstar, move, copy)
- Configure structured logging with timestamps and module names, replacing print statements
- Register admin and starred routers, add global HTTPException handler with JSON error responses
- Update authenticate_user and create_access_token to handle 2FA verification and token claims
2025-11-10 01:58:41 +01:00
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=exc.status_code,
|
2025-11-13 23:05:26 +01:00
|
|
|
content=ErrorResponse(code=exc.status_code, message=exc.detail).model_dump(),
|
2025-11-14 01:59:01 +01:00
|
|
|
headers=headers,
|
feat: add 2fa support, starred items, batch operations, and logging infrastructure
- Implement two-factor authentication with TOTP verification, recovery codes, and QR code setup in auth flow
- Add is_starred and last_accessed_at fields to File and Folder models for bookmarking and recency tracking
- Introduce batch operation endpoints for files and folders (delete, star, unstar, move, copy)
- Configure structured logging with timestamps and module names, replacing print statements
- Register admin and starred routers, add global HTTPException handler with JSON error responses
- Update authenticate_user and create_access_token to handle 2FA verification and token claims
2025-11-10 01:58:41 +01:00
|
|
|
)
|
|
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
|
2025-11-16 01:48:39 +01:00
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
|
|
|
async def splash_page(request: Request):
|
|
|
|
|
return templates.TemplateResponse("splash.html", {"request": request})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/features", response_class=HTMLResponse)
|
|
|
|
|
async def features_page(request: Request):
|
|
|
|
|
return templates.TemplateResponse("features.html", {"request": request})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/pricing", response_class=HTMLResponse)
|
|
|
|
|
async def pricing_page(request: Request):
|
|
|
|
|
return templates.TemplateResponse("pricing.html", {"request": request})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/support", response_class=HTMLResponse)
|
|
|
|
|
async def support_page(request: Request):
|
|
|
|
|
return templates.TemplateResponse("support.html", {"request": request})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/legal/{document_name}", response_class=HTMLResponse)
|
|
|
|
|
async def legal_document(request: Request, document_name: str):
|
|
|
|
|
try:
|
|
|
|
|
return templates.TemplateResponse(f"legal/{document_name}.html", {"request": request})
|
|
|
|
|
except Exception:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Legal document not found")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/login")
|
|
|
|
|
async def login_redirect():
|
|
|
|
|
return RedirectResponse(url="/app", status_code=302)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/app", response_class=HTMLResponse)
|
|
|
|
|
async def web_app():
|
2025-11-09 23:29:07 +01:00
|
|
|
with open("static/index.html", "r") as f:
|
|
|
|
|
return f.read()
|
|
|
|
|
|
2025-11-13 23:22:05 +01:00
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
def main():
|
2025-11-13 20:50:03 +01:00
|
|
|
parser = argparse.ArgumentParser(description="Run the MyWebdav application.")
|
2025-11-13 23:22:05 +01:00
|
|
|
parser.add_argument(
|
|
|
|
|
"--host", type=str, default="0.0.0.0", help="Host address to bind to"
|
|
|
|
|
)
|
2026-01-06 11:08:59 +01:00
|
|
|
parser.add_argument("--port", type=int, default=9004, help="Port to listen on")
|
2025-11-09 23:29:07 +01:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
|
|
uvicorn.run(app, host=args.host, port=args.port)
|
|
|
|
|
|
2025-11-13 23:22:05 +01:00
|
|
|
|
2025-11-09 23:29:07 +01:00
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|