mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
try: ip fix for cludflare
- Introduced AI File Sorting functionality to automatically organize documents into a smart folder hierarchy based on source, date, and topic. - Updated README.md to include the new feature. - Enhanced homepage components with new illustrations and descriptions for AI File Sorting. - Refactored rate limiting logic to extract real client IPs more accurately.
This commit is contained in:
parent
99995c67b2
commit
2cb30c604d
6 changed files with 143 additions and 12 deletions
|
|
@ -41,6 +41,7 @@ NotebookLM is one of the best and most useful AI platforms out there, but once y
|
||||||
- **No Vendor Lock-in** - Configure any LLM, image, TTS, and STT models to use.
|
- **No Vendor Lock-in** - Configure any LLM, image, TTS, and STT models to use.
|
||||||
- **25+ External Data Sources** - Add your sources from Google Drive, OneDrive, Dropbox, Notion, and many other external services.
|
- **25+ External Data Sources** - Add your sources from Google Drive, OneDrive, Dropbox, Notion, and many other external services.
|
||||||
- **Real-Time Multiplayer Support** - Work easily with your team members in a shared notebook.
|
- **Real-Time Multiplayer Support** - Work easily with your team members in a shared notebook.
|
||||||
|
- **AI File Sorting** - Automatically organize your documents into a smart folder hierarchy using AI-powered categorization by source, date, and topic.
|
||||||
- **Desktop App** - Get AI assistance in any application with Quick Assist, General Assist, Extreme Assist, and local folder sync.
|
- **Desktop App** - Get AI assistance in any application with Quick Assist, General Assist, Extreme Assist, and local folder sync.
|
||||||
|
|
||||||
...and more to come.
|
...and more to come.
|
||||||
|
|
@ -199,6 +200,7 @@ All features operate against your chosen search space, so your answers are alway
|
||||||
| **Video Generation** | Cinematic Video Overviews via Veo 3 (Ultra only) | Available (NotebookLM is better here, actively improving) |
|
| **Video Generation** | Cinematic Video Overviews via Veo 3 (Ultra only) | Available (NotebookLM is better here, actively improving) |
|
||||||
| **Presentation Generation** | Better looking slides but not editable | Create editable, slide-based presentations |
|
| **Presentation Generation** | Better looking slides but not editable | Create editable, slide-based presentations |
|
||||||
| **Podcast Generation** | Audio Overviews with customizable hosts and languages | Available with multiple TTS providers (NotebookLM is better here, actively improving) |
|
| **Podcast Generation** | Audio Overviews with customizable hosts and languages | Available with multiple TTS providers (NotebookLM is better here, actively improving) |
|
||||||
|
| **AI File Sorting** | No | LLM-powered auto-categorization into source, date, category, and subcategory folders |
|
||||||
| **Desktop App** | No | Native app with General Assist, Quick Assist, Extreme Assist, and local folder sync |
|
| **Desktop App** | No | Native app with General Assist, Quick Assist, Extreme Assist, and local folder sync |
|
||||||
| **Browser Extension** | No | Cross-browser extension to save any webpage, including auth-protected pages |
|
| **Browser Extension** | No | Cross-browser extension to save any webpage, including auth-protected pages |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from slowapi.errors import RateLimitExceeded
|
from slowapi.errors import RateLimitExceeded
|
||||||
from slowapi.middleware import SlowAPIMiddleware
|
from slowapi.middleware import SlowAPIMiddleware
|
||||||
from slowapi.util import get_remote_address
|
from slowapi.util import get_remote_address # noqa: F401 — kept for reference
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||||
from starlette.requests import Request as StarletteRequest
|
from starlette.requests import Request as StarletteRequest
|
||||||
|
|
@ -35,7 +35,7 @@ from app.config import (
|
||||||
)
|
)
|
||||||
from app.db import User, create_db_and_tables, get_async_session
|
from app.db import User, create_db_and_tables, get_async_session
|
||||||
from app.exceptions import GENERIC_5XX_MESSAGE, ISSUES_URL, SurfSenseError
|
from app.exceptions import GENERIC_5XX_MESSAGE, ISSUES_URL, SurfSenseError
|
||||||
from app.rate_limiter import limiter
|
from app.rate_limiter import get_real_client_ip, limiter
|
||||||
from app.routes import router as crud_router
|
from app.routes import router as crud_router
|
||||||
from app.routes.auth_routes import router as auth_router
|
from app.routes.auth_routes import router as auth_router
|
||||||
from app.schemas import UserCreate, UserRead, UserUpdate
|
from app.schemas import UserCreate, UserRead, UserUpdate
|
||||||
|
|
@ -290,7 +290,7 @@ def _check_rate_limit(
|
||||||
Uses atomic INCR + EXPIRE to avoid race conditions.
|
Uses atomic INCR + EXPIRE to avoid race conditions.
|
||||||
Falls back to in-memory sliding window if Redis is unavailable.
|
Falls back to in-memory sliding window if Redis is unavailable.
|
||||||
"""
|
"""
|
||||||
client_ip = get_remote_address(request)
|
client_ip = get_real_client_ip(request)
|
||||||
key = f"surfsense:auth_rate_limit:{scope}:{client_ip}"
|
key = f"surfsense:auth_rate_limit:{scope}:{client_ip}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,33 @@
|
||||||
"""Shared SlowAPI limiter instance used by app.py and route modules."""
|
"""Shared SlowAPI limiter instance used by app.py and route modules."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from limits.storage import MemoryStorage
|
from limits.storage import MemoryStorage
|
||||||
from slowapi import Limiter
|
from slowapi import Limiter
|
||||||
from slowapi.util import get_remote_address
|
from starlette.requests import Request
|
||||||
|
|
||||||
from app.config import config
|
from app.config import config
|
||||||
|
|
||||||
|
|
||||||
|
def get_real_client_ip(request: Request) -> str:
|
||||||
|
"""Extract the real client IP behind Cloudflare / reverse proxies.
|
||||||
|
|
||||||
|
Priority: CF-Connecting-IP > X-Real-IP > X-Forwarded-For (first entry) > socket peer.
|
||||||
|
"""
|
||||||
|
cf_ip = request.headers.get("cf-connecting-ip")
|
||||||
|
if cf_ip:
|
||||||
|
return cf_ip.strip()
|
||||||
|
real_ip = request.headers.get("x-real-ip")
|
||||||
|
if real_ip:
|
||||||
|
return real_ip.strip()
|
||||||
|
forwarded = request.headers.get("x-forwarded-for")
|
||||||
|
if forwarded:
|
||||||
|
return forwarded.split(",")[0].strip()
|
||||||
|
return request.client.host if request.client else "127.0.0.1"
|
||||||
|
|
||||||
|
|
||||||
limiter = Limiter(
|
limiter = Limiter(
|
||||||
key_func=get_remote_address,
|
key_func=get_real_client_ip,
|
||||||
storage_uri=config.REDIS_APP_URL,
|
storage_uri=config.REDIS_APP_URL,
|
||||||
default_limits=["1024/minute"],
|
default_limits=["1024/minute"],
|
||||||
in_memory_fallback_enabled=True,
|
in_memory_fallback_enabled=True,
|
||||||
|
|
|
||||||
|
|
@ -51,12 +51,17 @@ def _get_or_create_session_id(request: Request, response: Response) -> str:
|
||||||
|
|
||||||
|
|
||||||
def _get_client_ip(request: Request) -> str:
|
def _get_client_ip(request: Request) -> str:
|
||||||
|
"""Extract the real client IP, preferring Cloudflare's header."""
|
||||||
|
cf_ip = request.headers.get("cf-connecting-ip")
|
||||||
|
if cf_ip:
|
||||||
|
return cf_ip.strip()
|
||||||
|
real_ip = request.headers.get("x-real-ip")
|
||||||
|
if real_ip:
|
||||||
|
return real_ip.strip()
|
||||||
forwarded = request.headers.get("x-forwarded-for")
|
forwarded = request.headers.get("x-forwarded-for")
|
||||||
return (
|
if forwarded:
|
||||||
forwarded.split(",")[0].strip()
|
return forwarded.split(",")[0].strip()
|
||||||
if forwarded
|
return request.client.host if request.client else "unknown"
|
||||||
else (request.client.host if request.client else "unknown")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,10 @@
|
||||||
import { IconMessage, IconMicrophone, IconSearch, IconUsers } from "@tabler/icons-react";
|
import {
|
||||||
|
IconBinaryTree,
|
||||||
|
IconMessage,
|
||||||
|
IconMicrophone,
|
||||||
|
IconSearch,
|
||||||
|
IconUsers,
|
||||||
|
} from "@tabler/icons-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { BentoGrid, BentoGridItem } from "@/components/ui/bento-grid";
|
import { BentoGrid, BentoGridItem } from "@/components/ui/bento-grid";
|
||||||
|
|
@ -414,6 +420,91 @@ const AudioCommentIllustration = () => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const AiSortIllustration = () => (
|
||||||
|
<div className="relative flex w-full h-full min-h-[6rem] items-center justify-center overflow-hidden rounded-xl bg-gradient-to-br from-emerald-50 via-teal-50 to-cyan-50 dark:from-emerald-950/20 dark:via-teal-950/20 dark:to-cyan-950/20 p-4">
|
||||||
|
<svg viewBox="0 0 400 200" className="w-full h-full" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<title>AI File Sorting illustration showing automatic folder organization</title>
|
||||||
|
{/* Scattered documents on the left */}
|
||||||
|
<g opacity="0.5">
|
||||||
|
<rect x="20" y="40" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(-8 37 62)" />
|
||||||
|
<rect x="50" y="80" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(5 67 102)" />
|
||||||
|
<rect x="15" y="110" width="35" height="45" rx="4" className="fill-neutral-200 dark:fill-neutral-700" transform="rotate(-3 32 132)" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* AI sparkle / magic in the center */}
|
||||||
|
<g transform="translate(140, 90)">
|
||||||
|
<path d="M 0,-18 L 4,-6 L 16,-4 L 6,4 L 8,16 L 0,10 L -8,16 L -6,4 L -16,-4 L -4,-6 Z" className="fill-emerald-500 dark:fill-emerald-400" opacity="0.85">
|
||||||
|
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="10s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
<circle cx="0" cy="0" r="3" className="fill-white dark:fill-emerald-200">
|
||||||
|
<animate attributeName="opacity" values="0.5;1;0.5" dur="2s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Animated sorting arrows */}
|
||||||
|
<g className="stroke-emerald-500 dark:stroke-emerald-400" strokeWidth="2" fill="none" opacity="0.6">
|
||||||
|
<path d="M 100 70 Q 140 60, 180 50" strokeDasharray="4,4">
|
||||||
|
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
<path d="M 100 100 Q 140 100, 180 100" strokeDasharray="4,4">
|
||||||
|
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
<path d="M 100 130 Q 140 140, 180 150" strokeDasharray="4,4">
|
||||||
|
<animate attributeName="stroke-dashoffset" from="8" to="0" dur="1s" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Organized folder tree on the right */}
|
||||||
|
{/* Root folder */}
|
||||||
|
<g>
|
||||||
|
<rect x="220" y="30" width="160" height="28" rx="6" className="fill-white dark:fill-neutral-800" opacity="0.9" />
|
||||||
|
<rect x="228" y="36" width="16" height="14" rx="3" className="fill-emerald-500 dark:fill-emerald-400" />
|
||||||
|
<line x1="252" y1="43" x2="330" y2="43" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2.5" strokeLinecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Subfolder 1 */}
|
||||||
|
<g>
|
||||||
|
<line x1="240" y1="58" x2="240" y2="76" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
||||||
|
<line x1="240" y1="76" x2="250" y2="76" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
||||||
|
<rect x="250" y="64" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
|
||||||
|
<rect x="257" y="70" width="12" height="11" rx="2" className="fill-teal-400 dark:fill-teal-500" />
|
||||||
|
<line x1="276" y1="76" x2="340" y2="76" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Subfolder 2 */}
|
||||||
|
<g>
|
||||||
|
<line x1="240" y1="76" x2="240" y2="108" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
||||||
|
<line x1="240" y1="108" x2="250" y2="108" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
||||||
|
<rect x="250" y="96" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
|
||||||
|
<rect x="257" y="102" width="12" height="11" rx="2" className="fill-cyan-400 dark:fill-cyan-500" />
|
||||||
|
<line x1="276" y1="108" x2="350" y2="108" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Subfolder 3 */}
|
||||||
|
<g>
|
||||||
|
<line x1="240" y1="108" x2="240" y2="140" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
||||||
|
<line x1="240" y1="140" x2="250" y2="140" className="stroke-neutral-300 dark:stroke-neutral-600" strokeWidth="1.5" />
|
||||||
|
<rect x="250" y="128" width="130" height="24" rx="5" className="fill-white dark:fill-neutral-800" opacity="0.85" />
|
||||||
|
<rect x="257" y="134" width="12" height="11" rx="2" className="fill-emerald-400 dark:fill-emerald-500" />
|
||||||
|
<line x1="276" y1="140" x2="325" y2="140" className="stroke-neutral-400 dark:stroke-neutral-500" strokeWidth="2" strokeLinecap="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* Sparkle accents */}
|
||||||
|
<g className="opacity-60">
|
||||||
|
<circle cx="170" cy="45" r="2" className="fill-emerald-400">
|
||||||
|
<animate attributeName="opacity" values="0;1;0" dur="2s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="190" cy="155" r="1.5" className="fill-teal-400">
|
||||||
|
<animate attributeName="opacity" values="0;1;0" dur="2.5s" begin="0.8s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
<circle cx="155" cy="120" r="1.5" className="fill-cyan-400">
|
||||||
|
<animate attributeName="opacity" values="0;1;0" dur="3s" begin="0.4s" repeatCount="indefinite" />
|
||||||
|
</circle>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
title: "Find, Ask, Act",
|
title: "Find, Ask, Act",
|
||||||
|
|
@ -431,6 +522,14 @@ const items = [
|
||||||
className: "md:col-span-1",
|
className: "md:col-span-1",
|
||||||
icon: <IconUsers className="h-4 w-4 text-neutral-500" />,
|
icon: <IconUsers className="h-4 w-4 text-neutral-500" />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "AI File Sorting",
|
||||||
|
description:
|
||||||
|
"Automatically organize documents into a smart folder hierarchy using AI-powered categorization by source, date, and topic.",
|
||||||
|
header: <AiSortIllustration />,
|
||||||
|
className: "md:col-span-1",
|
||||||
|
icon: <IconBinaryTree className="h-4 w-4 text-neutral-500" />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: "Collaborate Beyond Text",
|
title: "Collaborate Beyond Text",
|
||||||
description:
|
description:
|
||||||
|
|
@ -443,7 +542,7 @@ const items = [
|
||||||
title: "Context Where It Counts",
|
title: "Context Where It Counts",
|
||||||
description: "Add comments directly to your chats and docs for clear, in-the-moment feedback.",
|
description: "Add comments directly to your chats and docs for clear, in-the-moment feedback.",
|
||||||
header: <AnnotationIllustration />,
|
header: <AnnotationIllustration />,
|
||||||
className: "md:col-span-2",
|
className: "md:col-span-1",
|
||||||
icon: <IconMessage className="h-4 w-4 text-neutral-500" />,
|
icon: <IconMessage className="h-4 w-4 text-neutral-500" />,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -348,6 +348,11 @@ const comparisonRows: {
|
||||||
notebookLm: false,
|
notebookLm: false,
|
||||||
surfSense: true,
|
surfSense: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
feature: "AI File Sorting",
|
||||||
|
notebookLm: false,
|
||||||
|
surfSense: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function ComparisonStrip() {
|
function ComparisonStrip() {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue