render hosting

This commit is contained in:
Gamosoft 2025-11-25 15:26:58 +01:00
parent 3a96c46b24
commit dfa3710dc6
3 changed files with 69 additions and 9 deletions

View File

@ -16,6 +16,9 @@ from typing import List, Optional
import aiofiles import aiofiles
from datetime import datetime from datetime import datetime
import bcrypt import bcrypt
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from .utils import ( from .utils import (
get_all_notes, get_all_notes,
@ -77,6 +80,24 @@ app.add_middleware(
https_only=False # Set to True if using HTTPS https_only=False # Set to True if using HTTPS
) )
# Rate limiting (enabled via environment variable for demo deployments)
ENABLE_RATE_LIMITING = os.getenv('ENABLE_RATE_LIMITING', 'false').lower() == 'true'
if ENABLE_RATE_LIMITING:
limiter = Limiter(key_func=get_remote_address, default_limits=["200/hour"])
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
print("⚠️ Rate limiting ENABLED (ENABLE_RATE_LIMITING=true)")
else:
# Create a dummy limiter that doesn't actually limit
class DummyLimiter:
def limit(self, *args, **kwargs):
def decorator(func):
return func
return decorator
limiter = DummyLimiter()
print("✅ Rate limiting DISABLED (set ENABLE_RATE_LIMITING=true to enable)")
# Ensure required directories exist # Ensure required directories exist
ensure_directories(config) ensure_directories(config)
@ -436,7 +457,8 @@ async def get_theme(theme_id: str):
@api_router.post("/folders") @api_router.post("/folders")
async def create_new_folder(data: dict): @limiter.limit("30/minute")
async def create_new_folder(request: Request, data: dict):
"""Create a new folder""" """Create a new folder"""
try: try:
folder_path = data.get('path', '') folder_path = data.get('path', '')
@ -488,7 +510,8 @@ async def get_image(image_path: str):
@api_router.post("/upload-image") @api_router.post("/upload-image")
async def upload_image(file: UploadFile = File(...), note_path: str = Form(...)): @limiter.limit("20/minute")
async def upload_image(request: Request, file: UploadFile = File(...), note_path: str = Form(...)):
""" """
Upload an image file and save it to the attachments directory. Upload an image file and save it to the attachments directory.
Returns the relative path to the image for markdown linking. Returns the relative path to the image for markdown linking.
@ -543,7 +566,8 @@ async def upload_image(file: UploadFile = File(...), note_path: str = Form(...))
@api_router.post("/notes/move") @api_router.post("/notes/move")
async def move_note_endpoint(data: dict): @limiter.limit("30/minute")
async def move_note_endpoint(request: Request, data: dict):
"""Move a note to a different folder""" """Move a note to a different folder"""
try: try:
old_path = data.get('oldPath', '') old_path = data.get('oldPath', '')
@ -571,7 +595,8 @@ async def move_note_endpoint(data: dict):
@api_router.post("/folders/move") @api_router.post("/folders/move")
async def move_folder_endpoint(data: dict): @limiter.limit("20/minute")
async def move_folder_endpoint(request: Request, data: dict):
"""Move a folder to a different location""" """Move a folder to a different location"""
try: try:
old_path = data.get('oldPath', '') old_path = data.get('oldPath', '')
@ -596,7 +621,8 @@ async def move_folder_endpoint(data: dict):
@api_router.post("/folders/rename") @api_router.post("/folders/rename")
async def rename_folder_endpoint(data: dict): @limiter.limit("30/minute")
async def rename_folder_endpoint(request: Request, data: dict):
"""Rename a folder""" """Rename a folder"""
try: try:
old_path = data.get('oldPath', '') old_path = data.get('oldPath', '')
@ -621,7 +647,8 @@ async def rename_folder_endpoint(data: dict):
@api_router.delete("/folders/{folder_path:path}") @api_router.delete("/folders/{folder_path:path}")
async def delete_folder_endpoint(folder_path: str): @limiter.limit("20/minute")
async def delete_folder_endpoint(request: Request, folder_path: str):
"""Delete a folder and all its contents""" """Delete a folder and all its contents"""
try: try:
if not folder_path: if not folder_path:
@ -718,7 +745,8 @@ async def get_note(note_path: str):
@api_router.post("/notes/{note_path:path}") @api_router.post("/notes/{note_path:path}")
async def create_or_update_note(note_path: str, content: dict): @limiter.limit("60/minute")
async def create_or_update_note(request: Request, note_path: str, content: dict):
"""Create or update a note""" """Create or update a note"""
try: try:
note_content = content.get('content', '') note_content = content.get('content', '')
@ -756,7 +784,8 @@ async def create_or_update_note(note_path: str, content: dict):
@api_router.delete("/notes/{note_path:path}") @api_router.delete("/notes/{note_path:path}")
async def remove_note(note_path: str): @limiter.limit("30/minute")
async def remove_note(request: Request, note_path: str):
"""Delete a note""" """Delete a note"""
try: try:
success = delete_note(config['storage']['notes_dir'], note_path) success = delete_note(config['storage']['notes_dir'], note_path)
@ -839,7 +868,8 @@ async def calculate_note_stats(content: str):
@api_router.post("/plugins/{plugin_name}/toggle") @api_router.post("/plugins/{plugin_name}/toggle")
async def toggle_plugin(plugin_name: str, enabled: dict): @limiter.limit("10/minute")
async def toggle_plugin(request: Request, plugin_name: str, enabled: dict):
"""Enable or disable a plugin""" """Enable or disable a plugin"""
try: try:
is_enabled = enabled.get('enabled', False) is_enabled = enabled.get('enabled', False)

29
render.yaml Normal file
View File

@ -0,0 +1,29 @@
services:
- type: web
name: notediscovery-demo
env: docker
# OPTION 1: Pull pre-built image from GHCR (Recommended - faster, consistent)
# Uses images built by GitHub Actions and published to ghcr.io
# Deployment: Click "Manual Deploy" in Render Dashboard to pull latest
image:
url: ghcr.io/gamosoft/notediscovery:latest
# OPTION 2: Build from source (Fallback if GHCR unavailable)
# Comment out 'image' above and uncomment these lines to build on Render:
# dockerfilePath: ./Dockerfile
# dockerContext: .
plan: free
region: oregon
healthCheckPath: /health
envVars:
- key: PORT
value: 8000
- key: ENABLE_RATE_LIMITING
value: "true"
- key: DEMO_MODE
value: "true"
# Manual deployment only
autoDeploy: false

View File

@ -7,4 +7,5 @@ aiofiles==23.2.1
cryptography==41.0.7 cryptography==41.0.7
bcrypt==4.1.2 bcrypt==4.1.2
itsdangerous==2.1.2 itsdangerous==2.1.2
slowapi==0.1.9