mirror of
https://github.com/MODSetter/SurfSense.git
synced 2025-09-01 10:09:08 +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:
|
||||
- PGVector setup
|
||||
- Google OAuth configuration
|
||||
- Unstructured.io API key
|
||||
- Other required API keys
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"])
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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 { 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">
|
||||
|
|
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 { 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>
|
||||
);
|
||||
};
|
||||
}
|
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 { 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>
|
||||
);
|
||||
}
|
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";
|
||||
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>
|
||||
)}
|
||||
|
|
|
@ -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**
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue