mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-04 11:39:19 +00:00
feat: Removed Hard Dependecy on Google Auth
- Introduced LOCAL auth mode
This commit is contained in:
parent
c290146a8d
commit
521ee4a1c4
17 changed files with 535 additions and 125 deletions
|
@ -130,7 +130,6 @@ Both installation guides include detailed OS-specific instructions for Windows,
|
||||||
|
|
||||||
Before installation, make sure to complete the [prerequisite setup steps](https://www.surfsense.net/docs/) including:
|
Before installation, make sure to complete the [prerequisite setup steps](https://www.surfsense.net/docs/) including:
|
||||||
- PGVector setup
|
- PGVector setup
|
||||||
- Google OAuth configuration
|
|
||||||
- Unstructured.io API key
|
- Unstructured.io API key
|
||||||
- Other required API keys
|
- Other required API keys
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense"
|
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense"
|
||||||
|
|
||||||
SECRET_KEY="SECRET"
|
SECRET_KEY="SECRET"
|
||||||
GOOGLE_OAUTH_CLIENT_ID="924507538m"
|
|
||||||
GOOGLE_OAUTH_CLIENT_SECRET="GOCSV"
|
|
||||||
NEXT_FRONTEND_URL="http://localhost:3000"
|
NEXT_FRONTEND_URL="http://localhost:3000"
|
||||||
|
|
||||||
|
#Auth
|
||||||
|
AUTH_TYPE="GOOGLE" or "LOCAL"
|
||||||
|
# For Google Auth Only
|
||||||
|
GOOGLE_OAUTH_CLIENT_ID="924507538m"
|
||||||
|
GOOGLE_OAUTH_CLIENT_SECRET="GOCSV"
|
||||||
|
|
||||||
|
#Embedding Model
|
||||||
EMBEDDING_MODEL="mixedbread-ai/mxbai-embed-large-v1"
|
EMBEDDING_MODEL="mixedbread-ai/mxbai-embed-large-v1"
|
||||||
|
|
||||||
RERANKERS_MODEL_NAME="ms-marco-MiniLM-L-12-v2"
|
RERANKERS_MODEL_NAME="ms-marco-MiniLM-L-12-v2"
|
||||||
|
|
|
@ -6,16 +6,18 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
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.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
|
|
||||||
from app.schemas import UserCreate, UserRead, UserUpdate
|
from app.schemas import UserCreate, UserRead, UserUpdate
|
||||||
|
|
||||||
|
|
||||||
|
from app.routes import router as crud_router
|
||||||
|
from app.config import config
|
||||||
|
|
||||||
from app.users import (
|
from app.users import (
|
||||||
SECRET,
|
SECRET,
|
||||||
auth_backend,
|
auth_backend,
|
||||||
fastapi_users,
|
fastapi_users,
|
||||||
google_oauth_client,
|
current_active_user
|
||||||
current_active_user,
|
|
||||||
)
|
)
|
||||||
from app.routes import router as crud_router
|
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
|
@ -59,16 +61,20 @@ app.include_router(
|
||||||
prefix="/users",
|
prefix="/users",
|
||||||
tags=["users"],
|
tags=["users"],
|
||||||
)
|
)
|
||||||
app.include_router(
|
|
||||||
fastapi_users.get_oauth_router(
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
google_oauth_client,
|
from app.users import google_oauth_client
|
||||||
auth_backend,
|
app.include_router(
|
||||||
SECRET,
|
fastapi_users.get_oauth_router(
|
||||||
is_verified_by_default=True
|
google_oauth_client,
|
||||||
),
|
auth_backend,
|
||||||
prefix="/auth/google",
|
SECRET,
|
||||||
tags=["auth"],
|
is_verified_by_default=True
|
||||||
)
|
),
|
||||||
|
prefix="/auth/google",
|
||||||
|
tags=["auth"],
|
||||||
|
)
|
||||||
|
|
||||||
app.include_router(crud_router, prefix="/api/v1", tags=["crud"])
|
app.include_router(crud_router, prefix="/api/v1", tags=["crud"])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -38,12 +38,17 @@ class Config:
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
DATABASE_URL = os.getenv("DATABASE_URL")
|
DATABASE_URL = os.getenv("DATABASE_URL")
|
||||||
|
|
||||||
# AUTH: Google OAuth
|
|
||||||
GOOGLE_OAUTH_CLIENT_ID = os.getenv("GOOGLE_OAUTH_CLIENT_ID")
|
|
||||||
GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
|
|
||||||
NEXT_FRONTEND_URL = os.getenv("NEXT_FRONTEND_URL")
|
NEXT_FRONTEND_URL = os.getenv("NEXT_FRONTEND_URL")
|
||||||
|
|
||||||
|
|
||||||
|
# AUTH: Google OAuth
|
||||||
|
AUTH_TYPE = os.getenv("AUTH_TYPE")
|
||||||
|
if AUTH_TYPE == "GOOGLE":
|
||||||
|
GOOGLE_OAUTH_CLIENT_ID = os.getenv("GOOGLE_OAUTH_CLIENT_ID")
|
||||||
|
GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
|
||||||
|
|
||||||
|
|
||||||
# LONG-CONTEXT LLMS
|
# LONG-CONTEXT LLMS
|
||||||
LONG_CONTEXT_LLM = os.getenv("LONG_CONTEXT_LLM")
|
LONG_CONTEXT_LLM = os.getenv("LONG_CONTEXT_LLM")
|
||||||
LONG_CONTEXT_LLM_API_BASE = os.getenv("LONG_CONTEXT_LLM_API_BASE")
|
LONG_CONTEXT_LLM_API_BASE = os.getenv("LONG_CONTEXT_LLM_API_BASE")
|
||||||
|
|
|
@ -3,11 +3,7 @@ from datetime import datetime, timezone
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
from fastapi_users.db import (
|
|
||||||
SQLAlchemyBaseOAuthAccountTableUUID,
|
|
||||||
SQLAlchemyBaseUserTableUUID,
|
|
||||||
SQLAlchemyUserDatabase,
|
|
||||||
)
|
|
||||||
from pgvector.sqlalchemy import Vector
|
from pgvector.sqlalchemy import Vector
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
ARRAY,
|
ARRAY,
|
||||||
|
@ -30,6 +26,18 @@ from app.config import config
|
||||||
from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
|
from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
|
||||||
from app.retriver.documents_hybrid_search import DocumentHybridSearchRetriever
|
from app.retriver.documents_hybrid_search import DocumentHybridSearchRetriever
|
||||||
|
|
||||||
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
|
from fastapi_users.db import (
|
||||||
|
SQLAlchemyBaseOAuthAccountTableUUID,
|
||||||
|
SQLAlchemyBaseUserTableUUID,
|
||||||
|
SQLAlchemyUserDatabase,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
from fastapi_users.db import (
|
||||||
|
SQLAlchemyBaseUserTableUUID,
|
||||||
|
SQLAlchemyUserDatabase,
|
||||||
|
)
|
||||||
|
|
||||||
DATABASE_URL = config.DATABASE_URL
|
DATABASE_URL = config.DATABASE_URL
|
||||||
|
|
||||||
|
|
||||||
|
@ -141,17 +149,22 @@ class SearchSourceConnector(BaseModel, TimestampMixin):
|
||||||
user_id = Column(UUID(as_uuid=True), ForeignKey("user.id", ondelete='CASCADE'), nullable=False)
|
user_id = Column(UUID(as_uuid=True), ForeignKey("user.id", ondelete='CASCADE'), nullable=False)
|
||||||
user = relationship("User", back_populates="search_source_connectors")
|
user = relationship("User", back_populates="search_source_connectors")
|
||||||
|
|
||||||
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
|
class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class User(SQLAlchemyBaseUserTableUUID, Base):
|
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||||
oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
|
oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
|
||||||
"OAuthAccount", lazy="joined"
|
"OAuthAccount", lazy="joined"
|
||||||
)
|
)
|
||||||
search_spaces = relationship("SearchSpace", back_populates="user")
|
search_spaces = relationship("SearchSpace", back_populates="user")
|
||||||
search_source_connectors = relationship("SearchSourceConnector", back_populates="user")
|
search_source_connectors = relationship("SearchSourceConnector", back_populates="user")
|
||||||
|
else:
|
||||||
|
class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||||
|
|
||||||
|
search_spaces = relationship("SearchSpace", back_populates="user")
|
||||||
|
search_source_connectors = relationship("SearchSourceConnector", back_populates="user")
|
||||||
|
|
||||||
|
|
||||||
engine = create_async_engine(DATABASE_URL)
|
engine = create_async_engine(DATABASE_URL)
|
||||||
|
@ -180,8 +193,12 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
|
||||||
yield session
|
yield session
|
||||||
|
|
||||||
|
|
||||||
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)
|
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||||
|
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)
|
||||||
|
else:
|
||||||
|
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
|
||||||
|
yield SQLAlchemyUserDatabase(session, User)
|
||||||
|
|
||||||
async def get_chucks_hybrid_search_retriever(session: AsyncSession = Depends(get_async_session)):
|
async def get_chucks_hybrid_search_retriever(session: AsyncSession = Depends(get_async_session)):
|
||||||
return ChucksHybridSearchRetriever(session)
|
return ChucksHybridSearchRetriever(session)
|
||||||
|
|
|
@ -10,8 +10,8 @@ from fastapi_users.authentication import (
|
||||||
JWTStrategy,
|
JWTStrategy,
|
||||||
)
|
)
|
||||||
from fastapi_users.db import SQLAlchemyUserDatabase
|
from fastapi_users.db import SQLAlchemyUserDatabase
|
||||||
from httpx_oauth.clients.google import GoogleOAuth2
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi_users.schemas import model_dump
|
||||||
from app.config import config
|
from app.config import config
|
||||||
from app.db import User, get_user_db
|
from app.db import User, get_user_db
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
@ -22,10 +22,13 @@ class BearerResponse(BaseModel):
|
||||||
|
|
||||||
SECRET = config.SECRET_KEY
|
SECRET = config.SECRET_KEY
|
||||||
|
|
||||||
google_oauth_client = GoogleOAuth2(
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
config.GOOGLE_OAUTH_CLIENT_ID,
|
from httpx_oauth.clients.google import GoogleOAuth2
|
||||||
config.GOOGLE_OAUTH_CLIENT_SECRET,
|
|
||||||
)
|
google_oauth_client = GoogleOAuth2(
|
||||||
|
config.GOOGLE_OAUTH_CLIENT_ID,
|
||||||
|
config.GOOGLE_OAUTH_CLIENT_SECRET,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
|
||||||
|
@ -79,7 +82,10 @@ class CustomBearerTransport(BearerTransport):
|
||||||
async def get_login_response(self, token: str) -> Response:
|
async def get_login_response(self, token: str) -> Response:
|
||||||
bearer_response = BearerResponse(access_token=token, token_type="bearer")
|
bearer_response = BearerResponse(access_token=token, token_type="bearer")
|
||||||
redirect_url = f"{config.NEXT_FRONTEND_URL}/auth/callback?token={bearer_response.access_token}"
|
redirect_url = f"{config.NEXT_FRONTEND_URL}/auth/callback?token={bearer_response.access_token}"
|
||||||
return RedirectResponse(redirect_url, status_code=302)
|
if config.AUTH_TYPE == "GOOGLE":
|
||||||
|
return RedirectResponse(redirect_url, status_code=302)
|
||||||
|
else:
|
||||||
|
return JSONResponse(model_dump(bearer_response))
|
||||||
|
|
||||||
bearer_transport = CustomBearerTransport(tokenUrl="auth/jwt/login")
|
bearer_transport = CustomBearerTransport(tokenUrl="auth/jwt/login")
|
||||||
|
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
|
NEXT_PUBLIC_FASTAPI_BACKEND_URL=http://localhost:8000
|
||||||
|
NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE=LOCAL or GOOGLE
|
|
@ -4,7 +4,7 @@ import React from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Plus, Search, Trash2, AlertCircle, Loader2 } from 'lucide-react'
|
import { Plus, Search, Trash2, AlertCircle, Loader2, LogOut } from 'lucide-react'
|
||||||
import { Tilt } from '@/components/ui/tilt'
|
import { Tilt } from '@/components/ui/tilt'
|
||||||
import { Spotlight } from '@/components/ui/spotlight'
|
import { Spotlight } from '@/components/ui/spotlight'
|
||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/Logo';
|
||||||
|
@ -145,11 +145,19 @@ const DashboardPage = () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
|
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
|
||||||
|
|
||||||
if (loading) return <LoadingScreen />;
|
if (loading) return <LoadingScreen />;
|
||||||
if (error) return <ErrorScreen message={error} />;
|
if (error) return <ErrorScreen message={error} />;
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.removeItem('surfsense_bearer_token');
|
||||||
|
router.push('/');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDeleteSearchSpace = async (id: number) => {
|
const handleDeleteSearchSpace = async (id: number) => {
|
||||||
// Send DELETE request to the API
|
// Send DELETE request to the API
|
||||||
try {
|
try {
|
||||||
|
@ -193,7 +201,18 @@ const DashboardPage = () => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ThemeTogglerComponent />
|
<div className="flex items-center space-x-3">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="h-9 w-9 rounded-full"
|
||||||
|
aria-label="Logout"
|
||||||
|
>
|
||||||
|
<LogOut className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
<ThemeTogglerComponent />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-6 mt-6">
|
<div className="flex flex-col space-y-6 mt-6">
|
||||||
|
|
43
surfsense_web/app/login/AmbientBackground.tsx
Normal file
43
surfsense_web/app/login/AmbientBackground.tsx
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
"use client";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const AmbientBackground = () => {
|
||||||
|
return (
|
||||||
|
<div className="pointer-events-none absolute left-0 top-0 z-0 h-screen w-screen">
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
transform: "translateY(-350px) rotate(-45deg)",
|
||||||
|
width: "560px",
|
||||||
|
height: "1380px",
|
||||||
|
background:
|
||||||
|
"radial-gradient(68.54% 68.72% at 55.02% 31.46%, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.02) 50%, rgba(59, 130, 246, 0) 100%)",
|
||||||
|
}}
|
||||||
|
className="absolute left-0 top-0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
transform: "rotate(-45deg) translate(5%, -50%)",
|
||||||
|
transformOrigin: "top left",
|
||||||
|
width: "240px",
|
||||||
|
height: "1380px",
|
||||||
|
background:
|
||||||
|
"radial-gradient(50% 50% at 50% 50%, rgba(59, 130, 246, 0.06) 0%, rgba(59, 130, 246, 0.02) 80%, transparent 100%)",
|
||||||
|
}}
|
||||||
|
className="absolute left-0 top-0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
borderRadius: "20px",
|
||||||
|
transform: "rotate(-45deg) translate(-180%, -70%)",
|
||||||
|
transformOrigin: "top left",
|
||||||
|
width: "240px",
|
||||||
|
height: "1380px",
|
||||||
|
background:
|
||||||
|
"radial-gradient(50% 50% at 50% 50%, rgba(59, 130, 246, 0.04) 0%, rgba(59, 130, 246, 0.02) 80%, transparent 100%)",
|
||||||
|
}}
|
||||||
|
className="absolute left-0 top-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,6 +3,7 @@ import React from "react";
|
||||||
import { IconBrandGoogleFilled } from "@tabler/icons-react";
|
import { IconBrandGoogleFilled } from "@tabler/icons-react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Logo } from "@/components/Logo";
|
import { Logo } from "@/components/Logo";
|
||||||
|
import { AmbientBackground } from "./AmbientBackground";
|
||||||
|
|
||||||
export function GoogleLoginButton() {
|
export function GoogleLoginButton() {
|
||||||
const handleGoogleLogin = () => {
|
const handleGoogleLogin = () => {
|
||||||
|
@ -88,47 +89,4 @@ export function GoogleLoginButton() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const AmbientBackground = () => {
|
|
||||||
return (
|
|
||||||
<div className="pointer-events-none absolute left-0 top-0 z-0 h-screen w-screen">
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
transform: "translateY(-350px) rotate(-45deg)",
|
|
||||||
width: "560px",
|
|
||||||
height: "1380px",
|
|
||||||
background:
|
|
||||||
"radial-gradient(68.54% 68.72% at 55.02% 31.46%, rgba(59, 130, 246, 0.08) 0%, rgba(59, 130, 246, 0.02) 50%, rgba(59, 130, 246, 0) 100%)",
|
|
||||||
}}
|
|
||||||
className="absolute left-0 top-0"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
transform: "rotate(-45deg) translate(5%, -50%)",
|
|
||||||
transformOrigin: "top left",
|
|
||||||
width: "240px",
|
|
||||||
height: "1380px",
|
|
||||||
background:
|
|
||||||
"radial-gradient(50% 50% at 50% 50%, rgba(59, 130, 246, 0.06) 0%, rgba(59, 130, 246, 0.02) 80%, transparent 100%)",
|
|
||||||
}}
|
|
||||||
className="absolute left-0 top-0"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
borderRadius: "20px",
|
|
||||||
transform: "rotate(-45deg) translate(-180%, -70%)",
|
|
||||||
transformOrigin: "top left",
|
|
||||||
width: "240px",
|
|
||||||
height: "1380px",
|
|
||||||
background:
|
|
||||||
"radial-gradient(50% 50% at 50% 50%, rgba(59, 130, 246, 0.04) 0%, rgba(59, 130, 246, 0.02) 80%, transparent 100%)",
|
|
||||||
}}
|
|
||||||
className="absolute left-0 top-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
114
surfsense_web/app/login/LocalLoginForm.tsx
Normal file
114
surfsense_web/app/login/LocalLoginForm.tsx
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export function LocalLoginForm() {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [authType, setAuthType] = useState<string | null>(null);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Get the auth type from environment variables
|
||||||
|
setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE");
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create form data for the API request
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
formData.append("username", username);
|
||||||
|
formData.append("password", password);
|
||||||
|
formData.append("grant_type", "password");
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/jwt/login`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
body: formData.toString(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.detail || "Failed to login");
|
||||||
|
}
|
||||||
|
|
||||||
|
router.push("/auth/callback?token=" + data.access_token);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "An error occurred during login");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 p-4 text-sm text-red-500 dark:bg-red-900/20 dark:text-red-200">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? "Signing in..." : "Sign in"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{authType === "LOCAL" && (
|
||||||
|
<div className="mt-4 text-center text-sm">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Don't have an account?{" "}
|
||||||
|
<Link href="/register" className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">
|
||||||
|
Register here
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,5 +1,67 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
import { GoogleLoginButton } from "./GoogleLoginButton";
|
import { GoogleLoginButton } from "./GoogleLoginButton";
|
||||||
|
import { LocalLoginForm } from "./LocalLoginForm";
|
||||||
|
import { Logo } from "@/components/Logo";
|
||||||
|
import { AmbientBackground } from "./AmbientBackground";
|
||||||
|
import { useSearchParams } from "next/navigation";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return <GoogleLoginButton />;
|
const [authType, setAuthType] = useState<string | null>(null);
|
||||||
|
const [registrationSuccess, setRegistrationSuccess] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check if the user was redirected from registration
|
||||||
|
if (searchParams.get("registered") === "true") {
|
||||||
|
setRegistrationSuccess(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the auth type from environment variables
|
||||||
|
setAuthType(process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE");
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
// Show loading state while determining auth type
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="relative w-full overflow-hidden">
|
||||||
|
<AmbientBackground />
|
||||||
|
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
|
||||||
|
<Logo className="rounded-md" />
|
||||||
|
<div className="mt-8 flex items-center space-x-2">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||||
|
<span className="text-muted-foreground">Loading...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authType === "GOOGLE") {
|
||||||
|
return <GoogleLoginButton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full overflow-hidden">
|
||||||
|
<AmbientBackground />
|
||||||
|
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
|
||||||
|
<Logo className="rounded-md" />
|
||||||
|
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
|
||||||
|
Sign In
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{registrationSuccess && (
|
||||||
|
<div className="mb-4 w-full rounded-md bg-green-50 p-4 text-sm text-green-500 dark:bg-green-900/20 dark:text-green-200">
|
||||||
|
Registration successful! You can now sign in with your credentials.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<LocalLoginForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
149
surfsense_web/app/register/page.tsx
Normal file
149
surfsense_web/app/register/page.tsx
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
"use client";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Logo } from "@/components/Logo";
|
||||||
|
import { AmbientBackground } from "../login/AmbientBackground";
|
||||||
|
|
||||||
|
export default function RegisterPage() {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Check authentication type and redirect if not LOCAL
|
||||||
|
useEffect(() => {
|
||||||
|
const authType = process.env.NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE || "GOOGLE";
|
||||||
|
if (authType !== "LOCAL") {
|
||||||
|
router.push("/login");
|
||||||
|
}
|
||||||
|
}, [router]);
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError("Passwords do not match");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
setError("");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/register`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
is_active: true,
|
||||||
|
is_superuser: false,
|
||||||
|
is_verified: false,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.detail || "Registration failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redirect to login page after successful registration
|
||||||
|
router.push("/login?registered=true");
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || "An error occurred during registration");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full overflow-hidden">
|
||||||
|
<AmbientBackground />
|
||||||
|
<div className="mx-auto flex h-screen max-w-lg flex-col items-center justify-center">
|
||||||
|
<Logo className="rounded-md" />
|
||||||
|
<h1 className="my-8 text-xl font-bold text-neutral-800 dark:text-neutral-100 md:text-4xl">
|
||||||
|
Create an Account
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="rounded-md bg-red-50 p-4 text-sm text-red-500 dark:bg-red-900/20 dark:text-red-200">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||||
|
Confirm Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
className="mt-1 block w-full rounded-md border border-gray-300 bg-white px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full rounded-md bg-blue-600 px-4 py-2 text-white shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? "Creating account..." : "Register"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-4 text-center text-sm">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
|
Already have an account?{" "}
|
||||||
|
<Link href="/login" className="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400">
|
||||||
|
Sign in
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { IconMenu2, IconX, IconBrandGoogleFilled } from "@tabler/icons-react";
|
import { IconMenu2, IconX, IconBrandGoogleFilled, IconUser } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
motion,
|
motion,
|
||||||
AnimatePresence,
|
AnimatePresence,
|
||||||
|
@ -62,7 +62,7 @@ export const Navbar = () => {
|
||||||
|
|
||||||
const DesktopNav = ({ navItems, visible }: NavbarProps) => {
|
const DesktopNav = ({ navItems, visible }: NavbarProps) => {
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
const handleGoogleLogin = () => {
|
const handleGoogleLogin = () => {
|
||||||
// Redirect to the login page
|
// Redirect to the login page
|
||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
|
@ -73,8 +73,8 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
|
||||||
onMouseLeave={() => setHoveredIndex(null)}
|
onMouseLeave={() => setHoveredIndex(null)}
|
||||||
animate={{
|
animate={{
|
||||||
backdropFilter: "blur(16px)",
|
backdropFilter: "blur(16px)",
|
||||||
background: visible
|
background: visible
|
||||||
? "rgba(var(--background-rgb), 0.8)"
|
? "rgba(var(--background-rgb), 0.8)"
|
||||||
: "rgba(var(--background-rgb), 0.6)",
|
: "rgba(var(--background-rgb), 0.6)",
|
||||||
width: visible ? "38%" : "80%",
|
width: visible ? "38%" : "80%",
|
||||||
height: visible ? "48px" : "64px",
|
height: visible ? "48px" : "64px",
|
||||||
|
@ -99,7 +99,7 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<Logo className="h-8 w-8 rounded-md" />
|
<Logo className="h-8 w-8 rounded-md" />
|
||||||
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
<span className="dark:text-white/90 text-gray-800 text-lg font-bold">SurfSense</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
|
@ -175,8 +175,8 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="hidden cursor-pointer md:flex items-center gap-2 rounded-full dark:bg-white/20 dark:hover:bg-white/30 dark:text-white bg-gray-100 hover:bg-gray-200 text-gray-800 border-0"
|
className="hidden cursor-pointer md:flex items-center gap-2 rounded-full dark:bg-white/20 dark:hover:bg-white/30 dark:text-white bg-gray-100 hover:bg-gray-200 text-gray-800 border-0"
|
||||||
>
|
>
|
||||||
<IconBrandGoogleFilled className="h-4 w-4" />
|
<IconUser className="h-4 w-4" />
|
||||||
<span>Sign in with Google</span>
|
<span>Sign in</span>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
@ -188,19 +188,19 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
|
||||||
|
|
||||||
const MobileNav = ({ navItems, visible }: NavbarProps) => {
|
const MobileNav = ({ navItems, visible }: NavbarProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
const handleGoogleLogin = () => {
|
const handleGoogleLogin = () => {
|
||||||
// Redirect to the login page
|
// Redirect to the login page
|
||||||
window.location.href = "./login";
|
window.location.href = "./login";
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<motion.div
|
<motion.div
|
||||||
animate={{
|
animate={{
|
||||||
backdropFilter: "blur(16px)",
|
backdropFilter: "blur(16px)",
|
||||||
background: visible
|
background: visible
|
||||||
? "rgba(var(--background-rgb), 0.8)"
|
? "rgba(var(--background-rgb), 0.8)"
|
||||||
: "rgba(var(--background-rgb), 0.6)",
|
: "rgba(var(--background-rgb), 0.6)",
|
||||||
width: visible ? "80%" : "90%",
|
width: visible ? "80%" : "90%",
|
||||||
y: visible ? 0 : 8,
|
y: visible ? 0 : 8,
|
||||||
|
@ -225,7 +225,7 @@ const MobileNav = ({ navItems, visible }: NavbarProps) => {
|
||||||
} as React.CSSProperties}
|
} as React.CSSProperties}
|
||||||
>
|
>
|
||||||
<div className="flex flex-row justify-between items-center w-full">
|
<div className="flex flex-row justify-between items-center w-full">
|
||||||
<Logo className="h-8 w-8 rounded-md" />
|
<Logo className="h-8 w-8 rounded-md" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ThemeTogglerComponent />
|
<ThemeTogglerComponent />
|
||||||
{open ? (
|
{open ? (
|
||||||
|
@ -278,8 +278,8 @@ const MobileNav = ({ navItems, visible }: NavbarProps) => {
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="flex cursor-pointer items-center gap-2 mt-4 w-full justify-center rounded-full dark:bg-white/20 dark:hover:bg-white/30 dark:text-white bg-gray-100 hover:bg-gray-200 text-gray-800 border-0"
|
className="flex cursor-pointer items-center gap-2 mt-4 w-full justify-center rounded-full dark:bg-white/20 dark:hover:bg-white/30 dark:text-white bg-gray-100 hover:bg-gray-200 text-gray-800 border-0"
|
||||||
>
|
>
|
||||||
<IconBrandGoogleFilled className="h-4 w-4" />
|
<IconUser className="h-4 w-4" />
|
||||||
<span>Sign in with Google</span>
|
<span>Sign in</span>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -82,8 +82,7 @@ Before you begin, ensure you have:
|
||||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| DATABASE_URL | PostgreSQL connection string (e.g., `postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense`) |
|
| DATABASE_URL | PostgreSQL connection string (e.g., `postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense`) |
|
||||||
| SECRET_KEY | JWT Secret key for authentication (should be a secure random string) |
|
| SECRET_KEY | JWT Secret key for authentication (should be a secure random string) |
|
||||||
| GOOGLE_OAUTH_CLIENT_ID | Google OAuth client ID obtained from Google Cloud Console |
|
| AUTH_TYPE | Authentication method: `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
|
||||||
| GOOGLE_OAUTH_CLIENT_SECRET | Google OAuth client secret obtained from Google Cloud Console |
|
|
||||||
| NEXT_FRONTEND_URL | URL where your frontend application is hosted (e.g., `http://localhost:3000`) |
|
| NEXT_FRONTEND_URL | URL where your frontend application is hosted (e.g., `http://localhost:3000`) |
|
||||||
| EMBEDDING_MODEL | Name of the embedding model (e.g., `openai://text-embedding-ada-002`, `anthropic://claude-v1`, `mixedbread-ai/mxbai-embed-large-v1`) |
|
| EMBEDDING_MODEL | Name of the embedding model (e.g., `openai://text-embedding-ada-002`, `anthropic://claude-v1`, `mixedbread-ai/mxbai-embed-large-v1`) |
|
||||||
| RERANKERS_MODEL_NAME | Name of the reranker model (e.g., `ms-marco-MiniLM-L-12-v2`) |
|
| RERANKERS_MODEL_NAME | Name of the reranker model (e.g., `ms-marco-MiniLM-L-12-v2`) |
|
||||||
|
@ -96,10 +95,21 @@ Before you begin, ensure you have:
|
||||||
| TTS_SERVICE | Text-to-Speech API provider for Podcasts (e.g., `openai/tts-1`, `azure/neural`, `vertex_ai/`). See [supported providers](https://docs.litellm.ai/docs/text_to_speech#supported-providers) |
|
| TTS_SERVICE | Text-to-Speech API provider for Podcasts (e.g., `openai/tts-1`, `azure/neural`, `vertex_ai/`). See [supported providers](https://docs.litellm.ai/docs/text_to_speech#supported-providers) |
|
||||||
| STT_SERVICE | Speech-to-Text API provider for Podcasts (e.g., `openai/whisper-1`). See [supported providers](https://docs.litellm.ai/docs/audio_transcription#supported-providers) |
|
| STT_SERVICE | Speech-to-Text API provider for Podcasts (e.g., `openai/whisper-1`). See [supported providers](https://docs.litellm.ai/docs/audio_transcription#supported-providers) |
|
||||||
|
|
||||||
Include API keys for the LLM providers you're using. For example:
|
|
||||||
|
|
||||||
- `OPENAI_API_KEY`: If using OpenAI models
|
Include API keys for your chosen LLM providers:
|
||||||
- `GEMINI_API_KEY`: If using Google Gemini models
|
|
||||||
|
| ENV VARIABLE | DESCRIPTION |
|
||||||
|
|--------------------|-----------------------------------------------------------------------------|
|
||||||
|
| `OPENAI_API_KEY` | Required if using OpenAI models |
|
||||||
|
| `GEMINI_API_KEY` | Required if using Google Gemini models |
|
||||||
|
| `ANTHROPIC_API_KEY`| Required if using Anthropic models |
|
||||||
|
|
||||||
|
### Google OAuth Configuration (if AUTH_TYPE=GOOGLE)
|
||||||
|
|
||||||
|
| ENV VARIABLE | DESCRIPTION |
|
||||||
|
|----------------------------|-----------------------------------------------------------------------------|
|
||||||
|
| `GOOGLE_OAUTH_CLIENT_ID` | Client ID from Google Cloud Console |
|
||||||
|
| `GOOGLE_OAUTH_CLIENT_SECRET` | Client secret from Google Cloud Console |
|
||||||
|
|
||||||
**Optional Backend LangSmith Observability:**
|
**Optional Backend LangSmith Observability:**
|
||||||
| ENV VARIABLE | DESCRIPTION |
|
| ENV VARIABLE | DESCRIPTION |
|
||||||
|
@ -125,6 +135,7 @@ For other LLM providers, refer to the [LiteLLM documentation](https://docs.litel
|
||||||
| ENV VARIABLE | DESCRIPTION |
|
| ENV VARIABLE | DESCRIPTION |
|
||||||
| ------------------------------- | ---------------------------------------------------------- |
|
| ------------------------------- | ---------------------------------------------------------- |
|
||||||
| NEXT_PUBLIC_FASTAPI_BACKEND_URL | URL of the backend service (e.g., `http://localhost:8000`) |
|
| NEXT_PUBLIC_FASTAPI_BACKEND_URL | URL of the backend service (e.g., `http://localhost:8000`) |
|
||||||
|
| NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE | Same value as set in backend AUTH_TYPE i.e `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
|
||||||
|
|
||||||
2. **Build and Start Containers**
|
2. **Build and Start Containers**
|
||||||
|
|
||||||
|
|
|
@ -47,9 +47,11 @@ See the [installation notes](https://github.com/pgvector/pgvector/tree/master#in
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Google OAuth Setup
|
## Google OAuth Setup (Optional)
|
||||||
|
|
||||||
SurfSense user management and authentication works on Google OAuth. Lets set it up.
|
SurfSense supports both Google OAuth and local email/password authentication. Google OAuth is optional - if you prefer local authentication, you can skip this section.
|
||||||
|
|
||||||
|
To set up Google OAuth:
|
||||||
|
|
||||||
1. Login to your [Google Developer Console](https://console.cloud.google.com/)
|
1. Login to your [Google Developer Console](https://console.cloud.google.com/)
|
||||||
2. Enable People API.
|
2. Enable People API.
|
||||||
|
|
|
@ -53,25 +53,37 @@ Edit the `.env` file and set the following variables:
|
||||||
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| DATABASE_URL | PostgreSQL connection string (e.g., `postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense`) |
|
| DATABASE_URL | PostgreSQL connection string (e.g., `postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense`) |
|
||||||
| SECRET_KEY | JWT Secret key for authentication (should be a secure random string) |
|
| SECRET_KEY | JWT Secret key for authentication (should be a secure random string) |
|
||||||
| GOOGLE_OAUTH_CLIENT_ID | Google OAuth client ID |
|
| AUTH_TYPE | Authentication method: `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
|
||||||
| GOOGLE_OAUTH_CLIENT_SECRET | Google OAuth client secret |
|
| NEXT_FRONTEND_URL | URL where your frontend application is hosted (e.g., `http://localhost:3000`) |
|
||||||
| NEXT_FRONTEND_URL | Frontend application URL (e.g., `http://localhost:3000`) |
|
|
||||||
| EMBEDDING_MODEL | Name of the embedding model (e.g., `openai://text-embedding-ada-002`, `anthropic://claude-v1`, `mixedbread-ai/mxbai-embed-large-v1`) |
|
| EMBEDDING_MODEL | Name of the embedding model (e.g., `openai://text-embedding-ada-002`, `anthropic://claude-v1`, `mixedbread-ai/mxbai-embed-large-v1`) |
|
||||||
| RERANKERS_MODEL_NAME | Name of the reranker model (e.g., `ms-marco-MiniLM-L-12-v2`) |
|
| RERANKERS_MODEL_NAME | Name of the reranker model (e.g., `ms-marco-MiniLM-L-12-v2`) |
|
||||||
| RERANKERS_MODEL_TYPE | Type of reranker model (e.g., `flashrank`) |
|
| RERANKERS_MODEL_TYPE | Type of reranker model (e.g., `flashrank`) |
|
||||||
| FAST_LLM | LiteLLM routed faster LLM (e.g., `openai/gpt-4o-mini`, `ollama/deepseek-r1:8b`) |
|
| FAST_LLM | LiteLLM routed smaller, faster LLM (e.g., `openai/gpt-4o-mini`, `ollama/deepseek-r1:8b`) |
|
||||||
| STRATEGIC_LLM | LiteLLM routed advanced LLM (e.g., `openai/gpt-4o`, `ollama/gemma3:12b`) |
|
| STRATEGIC_LLM | LiteLLM routed advanced LLM for complex tasks (e.g., `openai/gpt-4o`, `ollama/gemma3:12b`) |
|
||||||
| LONG_CONTEXT_LLM | LiteLLM routed long-context LLM (e.g., `gemini/gemini-2.0-flash`, `ollama/deepseek-r1:8b`) |
|
| LONG_CONTEXT_LLM | LiteLLM routed LLM for longer context windows (e.g., `gemini/gemini-2.0-flash`, `ollama/deepseek-r1:8b`) |
|
||||||
| UNSTRUCTURED_API_KEY | API key for Unstructured.io service |
|
| UNSTRUCTURED_API_KEY | API key for Unstructured.io service for document parsing |
|
||||||
| FIRECRAWL_API_KEY | API key for Firecrawl service (if using crawler) |
|
| FIRECRAWL_API_KEY | API key for Firecrawl service for web crawling |
|
||||||
| TTS_SERVICE | Text-to-Speech API provider for Podcasts (e.g., `openai/tts-1`, `azure/neural`, `vertex_ai/`). See [supported providers](https://docs.litellm.ai/docs/text_to_speech#supported-providers) |
|
| TTS_SERVICE | Text-to-Speech API provider for Podcasts (e.g., `openai/tts-1`, `azure/neural`, `vertex_ai/`). See [supported providers](https://docs.litellm.ai/docs/text_to_speech#supported-providers) |
|
||||||
| STT_SERVICE | Speech-to-Text API provider for Podcasts (e.g., `openai/whisper-1`). See [supported providers](https://docs.litellm.ai/docs/audio_transcription#supported-providers) |
|
| STT_SERVICE | Speech-to-Text API provider for Podcasts (e.g., `openai/whisper-1`). See [supported providers](https://docs.litellm.ai/docs/audio_transcription#supported-providers) |
|
||||||
|
|
||||||
**Important**: Since LLM calls are routed through LiteLLM, include API keys for the LLM providers you're using:
|
|
||||||
|
|
||||||
- For OpenAI models: `OPENAI_API_KEY`
|
Include API keys for your chosen LLM providers:
|
||||||
- For Google Gemini models: `GEMINI_API_KEY`
|
|
||||||
- For other providers, refer to the [LiteLLM documentation](https://docs.litellm.ai/docs/providers)
|
| ENV VARIABLE | DESCRIPTION |
|
||||||
|
|--------------------|-----------------------------------------------------------------------------|
|
||||||
|
| `OPENAI_API_KEY` | Required if using OpenAI models |
|
||||||
|
| `GEMINI_API_KEY` | Required if using Google Gemini models |
|
||||||
|
| `ANTHROPIC_API_KEY`| Required if using Anthropic models |
|
||||||
|
|
||||||
|
For other providers, refer to the [LiteLLM documentation](https://docs.litellm.ai/docs/providers)
|
||||||
|
|
||||||
|
### Google OAuth Configuration (if AUTH_TYPE=GOOGLE)
|
||||||
|
|
||||||
|
| ENV VARIABLE | DESCRIPTION |
|
||||||
|
|----------------------------|-----------------------------------------------------------------------------|
|
||||||
|
| `GOOGLE_OAUTH_CLIENT_ID` | Client ID from Google Cloud Console |
|
||||||
|
| `GOOGLE_OAUTH_CLIENT_SECRET` | Client secret from Google Cloud Console |
|
||||||
|
|
||||||
|
|
||||||
**Optional Backend LangSmith Observability:**
|
**Optional Backend LangSmith Observability:**
|
||||||
| ENV VARIABLE | DESCRIPTION |
|
| ENV VARIABLE | DESCRIPTION |
|
||||||
|
@ -169,6 +181,7 @@ Edit the `.env` file and set:
|
||||||
| ENV VARIABLE | DESCRIPTION |
|
| ENV VARIABLE | DESCRIPTION |
|
||||||
| ------------------------------- | ------------------------------------------- |
|
| ------------------------------- | ------------------------------------------- |
|
||||||
| NEXT_PUBLIC_FASTAPI_BACKEND_URL | Backend URL (e.g., `http://localhost:8000`) |
|
| NEXT_PUBLIC_FASTAPI_BACKEND_URL | Backend URL (e.g., `http://localhost:8000`) |
|
||||||
|
| NEXT_PUBLIC_FASTAPI_BACKEND_AUTH_TYPE | Same value as set in backend AUTH_TYPE i.e `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
|
||||||
|
|
||||||
### 2. Install Dependencies
|
### 2. Install Dependencies
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue