feat: Removed Hard Dependecy on Google Auth

- Introduced LOCAL auth mode
This commit is contained in:
DESKTOP-RTLN3BA\$punk 2025-05-21 20:56:23 -07:00
parent c290146a8d
commit 521ee4a1c4
17 changed files with 535 additions and 125 deletions

View file

@ -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:
- PGVector setup
- Google OAuth configuration
- Unstructured.io API key
- Other required API keys

View file

@ -1,10 +1,15 @@
DATABASE_URL="postgresql+asyncpg://postgres:postgres@localhost:5432/surfsense"
SECRET_KEY="SECRET"
GOOGLE_OAUTH_CLIENT_ID="924507538m"
GOOGLE_OAUTH_CLIENT_SECRET="GOCSV"
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"
RERANKERS_MODEL_NAME="ms-marco-MiniLM-L-12-v2"

View file

@ -6,16 +6,18 @@ from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.ext.asyncio import AsyncSession
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.routes import router as crud_router
from app.config import config
from app.users import (
SECRET,
auth_backend,
fastapi_users,
google_oauth_client,
current_active_user,
current_active_user
)
from app.routes import router as crud_router
@asynccontextmanager
@ -59,16 +61,20 @@ app.include_router(
prefix="/users",
tags=["users"],
)
app.include_router(
fastapi_users.get_oauth_router(
google_oauth_client,
auth_backend,
SECRET,
is_verified_by_default=True
),
prefix="/auth/google",
tags=["auth"],
)
if config.AUTH_TYPE == "GOOGLE":
from app.users import google_oauth_client
app.include_router(
fastapi_users.get_oauth_router(
google_oauth_client,
auth_backend,
SECRET,
is_verified_by_default=True
),
prefix="/auth/google",
tags=["auth"],
)
app.include_router(crud_router, prefix="/api/v1", tags=["crud"])

View file

@ -38,12 +38,17 @@ class Config:
# Database
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")
# 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_LLM = os.getenv("LONG_CONTEXT_LLM")
LONG_CONTEXT_LLM_API_BASE = os.getenv("LONG_CONTEXT_LLM_API_BASE")

View file

@ -3,11 +3,7 @@ from datetime import datetime, timezone
from enum import Enum
from fastapi import Depends
from fastapi_users.db import (
SQLAlchemyBaseOAuthAccountTableUUID,
SQLAlchemyBaseUserTableUUID,
SQLAlchemyUserDatabase,
)
from pgvector.sqlalchemy import Vector
from sqlalchemy import (
ARRAY,
@ -30,6 +26,18 @@ from app.config import config
from app.retriver.chunks_hybrid_search import ChucksHybridSearchRetriever
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
@ -141,17 +149,22 @@ class SearchSourceConnector(BaseModel, TimestampMixin):
user_id = Column(UUID(as_uuid=True), ForeignKey("user.id", ondelete='CASCADE'), nullable=False)
user = relationship("User", back_populates="search_source_connectors")
class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
pass
if config.AUTH_TYPE == "GOOGLE":
class OAuthAccount(SQLAlchemyBaseOAuthAccountTableUUID, Base):
pass
class User(SQLAlchemyBaseUserTableUUID, Base):
oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
"OAuthAccount", lazy="joined"
)
search_spaces = relationship("SearchSpace", back_populates="user")
search_source_connectors = relationship("SearchSourceConnector", back_populates="user")
class User(SQLAlchemyBaseUserTableUUID, Base):
oauth_accounts: Mapped[list[OAuthAccount]] = relationship(
"OAuthAccount", lazy="joined"
)
search_spaces = relationship("SearchSpace", 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)
@ -180,8 +193,12 @@ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
yield session
async def get_user_db(session: AsyncSession = Depends(get_async_session)):
yield SQLAlchemyUserDatabase(session, User, OAuthAccount)
if config.AUTH_TYPE == "GOOGLE":
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)):
return ChucksHybridSearchRetriever(session)

View file

@ -10,8 +10,8 @@ from fastapi_users.authentication import (
JWTStrategy,
)
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.db import User, get_user_db
from pydantic import BaseModel
@ -22,10 +22,13 @@ class BearerResponse(BaseModel):
SECRET = config.SECRET_KEY
google_oauth_client = GoogleOAuth2(
config.GOOGLE_OAUTH_CLIENT_ID,
config.GOOGLE_OAUTH_CLIENT_SECRET,
)
if config.AUTH_TYPE == "GOOGLE":
from httpx_oauth.clients.google import GoogleOAuth2
google_oauth_client = GoogleOAuth2(
config.GOOGLE_OAUTH_CLIENT_ID,
config.GOOGLE_OAUTH_CLIENT_SECRET,
)
class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
@ -79,7 +82,10 @@ class CustomBearerTransport(BearerTransport):
async def get_login_response(self, token: str) -> Response:
bearer_response = BearerResponse(access_token=token, token_type="bearer")
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")

View file

@ -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

View file

@ -4,7 +4,7 @@ import React from 'react'
import Link from 'next/link'
import { motion } from 'framer-motion'
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 { Spotlight } from '@/components/ui/spotlight'
import { Logo } from '@/components/Logo';
@ -145,11 +145,19 @@ const DashboardPage = () => {
},
};
const router = useRouter();
const { searchSpaces, loading, error, refreshSearchSpaces } = useSearchSpaces();
if (loading) return <LoadingScreen />;
if (error) return <ErrorScreen message={error} />;
const handleLogout = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('surfsense_bearer_token');
router.push('/');
}
};
const handleDeleteSearchSpace = async (id: number) => {
// Send DELETE request to the API
try {
@ -193,7 +201,18 @@ const DashboardPage = () => {
</p>
</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 className="flex flex-col space-y-6 mt-6">

View 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>
);
};

View file

@ -3,6 +3,7 @@ import React from "react";
import { IconBrandGoogleFilled } from "@tabler/icons-react";
import { motion } from "framer-motion";
import { Logo } from "@/components/Logo";
import { AmbientBackground } from "./AmbientBackground";
export function GoogleLoginButton() {
const handleGoogleLogin = () => {
@ -88,47 +89,4 @@ export function GoogleLoginButton() {
</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>
);
};
}

View 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&apos;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>
);
}

View file

@ -1,5 +1,67 @@
"use client";
import { useState, useEffect } from "react";
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() {
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>
);
}

View 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>
);
}

View file

@ -1,6 +1,6 @@
"use client";
import { cn } from "@/lib/utils";
import { IconMenu2, IconX, IconBrandGoogleFilled } from "@tabler/icons-react";
import { IconMenu2, IconX, IconBrandGoogleFilled, IconUser } from "@tabler/icons-react";
import {
motion,
AnimatePresence,
@ -62,7 +62,7 @@ export const Navbar = () => {
const DesktopNav = ({ navItems, visible }: NavbarProps) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const handleGoogleLogin = () => {
// Redirect to the login page
window.location.href = '/login';
@ -73,8 +73,8 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
onMouseLeave={() => setHoveredIndex(null)}
animate={{
backdropFilter: "blur(16px)",
background: visible
? "rgba(var(--background-rgb), 0.8)"
background: visible
? "rgba(var(--background-rgb), 0.8)"
: "rgba(var(--background-rgb), 0.6)",
width: visible ? "38%" : "80%",
height: visible ? "48px" : "64px",
@ -99,7 +99,7 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
} as React.CSSProperties}
>
<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>
</div>
<div className="flex items-center gap-4">
@ -175,8 +175,8 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
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"
>
<IconBrandGoogleFilled className="h-4 w-4" />
<span>Sign in with Google</span>
<IconUser className="h-4 w-4" />
<span>Sign in</span>
</Button>
</motion.div>
)}
@ -188,19 +188,19 @@ const DesktopNav = ({ navItems, visible }: NavbarProps) => {
const MobileNav = ({ navItems, visible }: NavbarProps) => {
const [open, setOpen] = useState(false);
const handleGoogleLogin = () => {
// Redirect to the login page
window.location.href = "./login";
};
return (
<>
<motion.div
animate={{
backdropFilter: "blur(16px)",
background: visible
? "rgba(var(--background-rgb), 0.8)"
background: visible
? "rgba(var(--background-rgb), 0.8)"
: "rgba(var(--background-rgb), 0.6)",
width: visible ? "80%" : "90%",
y: visible ? 0 : 8,
@ -225,7 +225,7 @@ const MobileNav = ({ navItems, visible }: NavbarProps) => {
} as React.CSSProperties}
>
<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">
<ThemeTogglerComponent />
{open ? (
@ -278,8 +278,8 @@ const MobileNav = ({ navItems, visible }: NavbarProps) => {
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"
>
<IconBrandGoogleFilled className="h-4 w-4" />
<span>Sign in with Google</span>
<IconUser className="h-4 w-4" />
<span>Sign in</span>
</Button>
</motion.div>
)}

View file

@ -82,8 +82,7 @@ Before you begin, ensure you have:
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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) |
| GOOGLE_OAUTH_CLIENT_ID | Google OAuth client ID obtained from Google Cloud Console |
| GOOGLE_OAUTH_CLIENT_SECRET | Google OAuth client secret obtained from Google Cloud Console |
| AUTH_TYPE | Authentication method: `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
| 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`) |
| 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) |
| 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
- `GEMINI_API_KEY`: If using Google Gemini models
Include API keys for your chosen LLM 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 |
### 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:**
| ENV VARIABLE | DESCRIPTION |
@ -125,6 +135,7 @@ For other LLM providers, refer to the [LiteLLM documentation](https://docs.litel
| ENV VARIABLE | DESCRIPTION |
| ------------------------------- | ---------------------------------------------------------- |
| 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**

View file

@ -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/)
2. Enable People API.

View file

@ -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`) |
| SECRET_KEY | JWT Secret key for authentication (should be a secure random string) |
| GOOGLE_OAUTH_CLIENT_ID | Google OAuth client ID |
| GOOGLE_OAUTH_CLIENT_SECRET | Google OAuth client secret |
| NEXT_FRONTEND_URL | Frontend application URL (e.g., `http://localhost:3000`) |
| AUTH_TYPE | Authentication method: `GOOGLE` for OAuth with Google, `LOCAL` for email/password authentication |
| 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`) |
| 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`) |
| FAST_LLM | LiteLLM routed 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`) |
| LONG_CONTEXT_LLM | LiteLLM routed long-context LLM (e.g., `gemini/gemini-2.0-flash`, `ollama/deepseek-r1:8b`) |
| UNSTRUCTURED_API_KEY | API key for Unstructured.io service |
| FIRECRAWL_API_KEY | API key for Firecrawl service (if using crawler) |
| FAST_LLM | LiteLLM routed smaller, faster LLM (e.g., `openai/gpt-4o-mini`, `ollama/deepseek-r1:8b`) |
| STRATEGIC_LLM | LiteLLM routed advanced LLM for complex tasks (e.g., `openai/gpt-4o`, `ollama/gemma3:12b`) |
| 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 for document parsing |
| 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) |
| 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`
- For Google Gemini models: `GEMINI_API_KEY`
- For other providers, refer to the [LiteLLM documentation](https://docs.litellm.ai/docs/providers)
Include API keys for your chosen LLM 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:**
| ENV VARIABLE | DESCRIPTION |
@ -169,6 +181,7 @@ Edit the `.env` file and set:
| ENV VARIABLE | DESCRIPTION |
| ------------------------------- | ------------------------------------------- |
| 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