From 2fd77b74c9906f0904f0481d7011ba98cf577742 Mon Sep 17 00:00:00 2001 From: samkul-swe Date: Thu, 18 Sep 2025 14:52:46 -0700 Subject: [PATCH 1/3] fix for 322 - shows relevant messages and toasts --- surfsense_web/app/login/LocalLoginForm.tsx | 145 +++++++++++++-- surfsense_web/app/login/page.tsx | 131 ++++++++++++- surfsense_web/app/register/page.tsx | 161 ++++++++++++++-- surfsense_web/lib/auth-errors.ts | 207 +++++++++++++++++++++ 4 files changed, 603 insertions(+), 41 deletions(-) create mode 100644 surfsense_web/lib/auth-errors.ts diff --git a/surfsense_web/app/login/LocalLoginForm.tsx b/surfsense_web/app/login/LocalLoginForm.tsx index 7cc50b28a..2a4d9239a 100644 --- a/surfsense_web/app/login/LocalLoginForm.tsx +++ b/surfsense_web/app/login/LocalLoginForm.tsx @@ -2,11 +2,15 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { toast } from "sonner"; +import { getAuthErrorDetails, shouldRetry, isNetworkError } from "@/lib/auth-errors"; export function LocalLoginForm() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); - const [error, setError] = useState(""); + const [error, setError] = useState(null); + const [errorTitle, setErrorTitle] = useState(null); const [isLoading, setIsLoading] = useState(false); const [authType, setAuthType] = useState(null); const router = useRouter(); @@ -19,7 +23,11 @@ export function LocalLoginForm() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); - setError(""); + setError(null); // Clear any previous errors + setErrorTitle(null); + + // Show loading toast + const loadingToast = toast.loading("Signing you in..."); try { // Create form data for the API request @@ -42,13 +50,54 @@ export function LocalLoginForm() { const data = await response.json(); if (!response.ok) { - throw new Error(data.detail || "Failed to login"); + throw new Error(data.detail || `HTTP ${response.status}`); } - router.push(`/auth/callback?token=${data.access_token}`); + // Success toast + toast.success("Login successful!", { + id: loadingToast, + description: "Redirecting to dashboard...", + duration: 2000, + }); + + // Small delay to show success message + setTimeout(() => { + router.push(`/auth/callback?token=${data.access_token}`); + }, 500); + } catch (err) { - const errorMessage = err instanceof Error ? err.message : "An error occurred during login"; - setError(errorMessage); + // Use auth-errors utility to get proper error details + let errorCode = "UNKNOWN_ERROR"; + + if (err instanceof Error) { + errorCode = err.message; + } else if (isNetworkError(err)) { + errorCode = "NETWORK_ERROR"; + } + + // Get detailed error information from auth-errors utility + const errorDetails = getAuthErrorDetails(errorCode); + + // Set persistent error display + setErrorTitle(errorDetails.title); + setError(errorDetails.description); + + // Show error toast with conditional retry action + const toastOptions: any = { + id: loadingToast, + description: errorDetails.description, + duration: 6000, + }; + + // Add retry action if the error is retryable + if (shouldRetry(errorCode)) { + toastOptions.action = { + label: "Retry", + onClick: () => handleSubmit(e), + }; + } + + toast.error(errorDetails.title, toastOptions); } finally { setIsLoading(false); } @@ -57,11 +106,69 @@ export function LocalLoginForm() { return (
- {error && ( -
- {error} -
- )} + {/* Error Display */} + + {error && errorTitle && ( + +
+ + Error Icon + + + + +
+

{errorTitle}

+

+ {error} +

+
+ +
+
+ )} +
@@ -93,14 +205,19 @@ export function LocalLoginForm() { 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" + className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ + error + ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" + : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" + }`} + disabled={isLoading} />
diff --git a/surfsense_web/app/login/page.tsx b/surfsense_web/app/login/page.tsx index 1957c22bf..209dc745a 100644 --- a/surfsense_web/app/login/page.tsx +++ b/surfsense_web/app/login/page.tsx @@ -3,6 +3,9 @@ import { Loader2 } from "lucide-react"; import { useSearchParams } from "next/navigation"; import { Suspense, useEffect, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { toast } from "sonner"; +import { getAuthErrorDetails, shouldRetry } from "@/lib/auth-errors"; import { Logo } from "@/components/Logo"; import { AmbientBackground } from "./AmbientBackground"; import { GoogleLoginButton } from "./GoogleLoginButton"; @@ -10,14 +13,70 @@ import { LocalLoginForm } from "./LocalLoginForm"; function LoginContent() { const [authType, setAuthType] = useState(null); - const [registrationSuccess, setRegistrationSuccess] = useState(false); const [isLoading, setIsLoading] = useState(true); + const [urlError, setUrlError] = useState<{ title: string; message: string } | null>(null); const searchParams = useSearchParams(); useEffect(() => { - // Check if the user was redirected from registration - if (searchParams.get("registered") === "true") { - setRegistrationSuccess(true); + // Check for various URL parameters that might indicate success or error states + const registered = searchParams.get("registered"); + const error = searchParams.get("error"); + const message = searchParams.get("message"); + const logout = searchParams.get("logout"); + + // Show registration success message + if (registered === "true") { + toast.success("Registration successful!", { + description: "You can now sign in with your credentials", + duration: 5000, + }); + } + + // Show logout confirmation + if (logout === "true") { + toast.success("Logged out successfully", { + description: "You have been securely logged out", + duration: 3000, + }); + } + + // Show error messages from OAuth or other flows using auth-errors utility + if (error) { + // Use the auth-errors utility to get proper error details + const errorDetails = getAuthErrorDetails(error); + + // If we have a custom message from URL params, use it as description + const errorDescription = message ? decodeURIComponent(message) : errorDetails.description; + + // Set persistent error display + setUrlError({ + title: errorDetails.title, + message: errorDescription + }); + + // Show toast with conditional retry action + const toastOptions: any = { + description: errorDescription, + duration: 6000, + }; + + // Add retry action if the error is retryable + if (shouldRetry(error)) { + toastOptions.action = { + label: "Retry", + onClick: () => window.location.reload(), + }; + } + + toast.error(errorDetails.title, toastOptions); + } + + // Show general messages + if (message && !error && !registered && !logout) { + toast.info("Notice", { + description: decodeURIComponent(message), + duration: 4000, + }); } // Get the auth type from environment variables @@ -54,11 +113,65 @@ function LoginContent() { Sign In - {registrationSuccess && ( -
- Registration successful! You can now sign in with your credentials. -
- )} + {/* URL Error Display */} + + {urlError && ( + +
+ + Error Icon + + + + +
+

{urlError.title}

+

+ {urlError.message} +

+
+ +
+
+ )} +
diff --git a/surfsense_web/app/register/page.tsx b/surfsense_web/app/register/page.tsx index 1a89dbb4e..28559c991 100644 --- a/surfsense_web/app/register/page.tsx +++ b/surfsense_web/app/register/page.tsx @@ -3,6 +3,9 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { toast } from "sonner"; +import { getAuthErrorDetails, shouldRetry, isNetworkError } from "@/lib/auth-errors"; import { Logo } from "@/components/Logo"; import { AmbientBackground } from "../login/AmbientBackground"; @@ -10,7 +13,8 @@ export default function RegisterPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); - const [error, setError] = useState(""); + const [error, setError] = useState(null); + const [errorTitle, setErrorTitle] = useState(null); const [isLoading, setIsLoading] = useState(false); const router = useRouter(); @@ -28,11 +32,20 @@ export default function RegisterPage() { // Form validation if (password !== confirmPassword) { setError("Passwords do not match"); + setErrorTitle("Password Mismatch"); + toast.error("Password Mismatch", { + description: "The passwords you entered do not match", + duration: 4000, + }); return; } setIsLoading(true); - setError(""); + setError(null); // Clear any previous errors + setErrorTitle(null); + + // Show loading toast + const loadingToast = toast.loading("Creating your account..."); try { const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/auth/register`, { @@ -52,15 +65,54 @@ export default function RegisterPage() { const data = await response.json(); if (!response.ok) { - throw new Error(data.detail || "Registration failed"); + throw new Error(data.detail || `HTTP ${response.status}`); } - // Redirect to login page after successful registration - router.push("/login?registered=true"); - } catch (err: unknown) { - const errorMessage = - err instanceof Error ? err.message : "An error occurred during registration"; - setError(errorMessage); + // Success toast + toast.success("Account created successfully!", { + id: loadingToast, + description: "Redirecting to login page...", + duration: 2000, + }); + + // Small delay to show success message + setTimeout(() => { + router.push("/login?registered=true"); + }, 500); + + } catch (err) { + // Use auth-errors utility to get proper error details + let errorCode = "UNKNOWN_ERROR"; + + if (err instanceof Error) { + errorCode = err.message; + } else if (isNetworkError(err)) { + errorCode = "NETWORK_ERROR"; + } + + // Get detailed error information from auth-errors utility + const errorDetails = getAuthErrorDetails(errorCode); + + // Set persistent error display + setErrorTitle(errorDetails.title); + setError(errorDetails.description); + + // Show error toast with conditional retry action + const toastOptions: any = { + id: loadingToast, + description: errorDetails.description, + duration: 6000, + }; + + // Add retry action if the error is retryable + if (shouldRetry(errorCode)) { + toastOptions.action = { + label: "Retry", + onClick: () => handleSubmit(e), + }; + } + + toast.error(errorDetails.title, toastOptions); } finally { setIsLoading(false); } @@ -77,11 +129,69 @@ export default function RegisterPage() {
- {error && ( -
- {error} -
- )} + {/* Enhanced Error Display */} + + {error && errorTitle && ( + +
+ + Error Icon + + + + +
+

{errorTitle}

+

+ {error} +

+
+ +
+
+ )} +
@@ -113,7 +228,12 @@ export default function RegisterPage() { 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" + className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ + error + ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" + : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" + }`} + disabled={isLoading} />
@@ -130,14 +250,19 @@ export default function RegisterPage() { 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" + className={`mt-1 block w-full rounded-md border px-3 py-2 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 dark:bg-gray-800 dark:text-white transition-colors ${ + error + ? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700" + : "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700" + }`} + disabled={isLoading} /> diff --git a/surfsense_web/lib/auth-errors.ts b/surfsense_web/lib/auth-errors.ts new file mode 100644 index 000000000..1858bd45c --- /dev/null +++ b/surfsense_web/lib/auth-errors.ts @@ -0,0 +1,207 @@ +/** + * Authentication error messages and handling utilities + */ + +interface AuthErrorMapping { + [key: string]: { + title: string; + description?: string; + }; +} + +const AUTH_ERROR_MESSAGES: AuthErrorMapping = { + // Common HTTP errors + "401": { + title: "Invalid credentials", + description: "Please check your email and password" + }, + "403": { + title: "Access denied", + description: "Your account may be suspended or restricted" + }, + "404": { + title: "Account not found", + description: "No account exists with this email address" + }, + "409": { + title: "Account conflict", + description: "An account with this email already exists" + }, + "429": { + title: "Too many attempts", + description: "Please wait before trying again" + }, + "500": { + title: "Server error", + description: "Something went wrong on our end. Please try again" + }, + "503": { + title: "Service unavailable", + description: "Login service is temporarily down" + }, + + // FastAPI specific errors + "LOGIN_BAD_CREDENTIALS": { + title: "Invalid credentials", + description: "The email or password you entered is incorrect" + }, + "LOGIN_USER_NOT_VERIFIED": { + title: "Account not verified", + description: "Please verify your email address before signing in" + }, + "USER_INACTIVE": { + title: "Account inactive", + description: "Your account has been deactivated. Contact support for assistance" + }, + "REGISTER_USER_ALREADY_EXISTS": { + title: "Account already exists", + description: "An account with this email address already exists" + }, + "REGISTER_INVALID_PASSWORD": { + title: "Invalid password", + description: "Password must meet security requirements" + }, + + // OAuth errors + "access_denied": { + title: "Access denied", + description: "You denied access or cancelled the login process" + }, + "invalid_request": { + title: "Invalid request", + description: "The login request was malformed" + }, + "unauthorized_client": { + title: "Authentication failed", + description: "The application is not authorized to perform this action" + }, + "unsupported_response_type": { + title: "Login method not supported", + description: "This login method is not currently available" + }, + "invalid_scope": { + title: "Invalid permissions", + description: "The requested permissions are not valid" + }, + "server_error": { + title: "Server error", + description: "An error occurred on the authentication server" + }, + "temporarily_unavailable": { + title: "Service unavailable", + description: "Login is temporarily unavailable. Please try again later" + }, + + // Network errors + "NETWORK_ERROR": { + title: "Connection failed", + description: "Please check your internet connection and try again" + }, + "TIMEOUT": { + title: "Request timeout", + description: "The login request took too long. Please try again" + }, + + // Generic fallbacks + "UNKNOWN_ERROR": { + title: "Login failed", + description: "An unexpected error occurred. Please try again" + } +}; + +/** + * Get a user-friendly error message for authentication errors + * @param errorCode - The error code or message from the API + * @param returnTitle - Whether to return just the title or full description + * @returns Formatted error message + */ +export function getAuthErrorMessage(errorCode: string, returnTitle: boolean = false): string { + if (!errorCode) { + const fallback = AUTH_ERROR_MESSAGES.UNKNOWN_ERROR; + return returnTitle ? fallback.title : fallback.description || fallback.title; + } + + // Clean up the error code + const cleanErrorCode = errorCode.trim().toUpperCase(); + + // Try exact match first + let errorInfo = AUTH_ERROR_MESSAGES[cleanErrorCode] || AUTH_ERROR_MESSAGES[errorCode]; + + // Try partial matches for HTTP status codes + if (!errorInfo) { + const statusCodeMatch = errorCode.match(/(\d{3})/); + if (statusCodeMatch) { + errorInfo = AUTH_ERROR_MESSAGES[statusCodeMatch[1]]; + } + } + + // Try partial matches for common error patterns + if (!errorInfo) { + const patterns = [ + { pattern: /credential|password|email/i, code: "LOGIN_BAD_CREDENTIALS" }, + { pattern: /verify|verification/i, code: "LOGIN_USER_NOT_VERIFIED" }, + { pattern: /inactive|disabled|suspended/i, code: "USER_INACTIVE" }, + { pattern: /exists|duplicate/i, code: "REGISTER_USER_ALREADY_EXISTS" }, + { pattern: /network|connection/i, code: "NETWORK_ERROR" }, + { pattern: /timeout/i, code: "TIMEOUT" }, + { pattern: /rate|limit|many/i, code: "429" }, + ]; + + for (const { pattern, code } of patterns) { + if (pattern.test(errorCode)) { + errorInfo = AUTH_ERROR_MESSAGES[code]; + break; + } + } + } + + // Fallback to unknown error + if (!errorInfo) { + errorInfo = AUTH_ERROR_MESSAGES.UNKNOWN_ERROR; + } + + return returnTitle ? errorInfo.title : errorInfo.description || errorInfo.title; +} + +/** + * Get both title and description for an error + * @param errorCode - The error code or message from the API + * @returns Object with title and description + */ +export function getAuthErrorDetails(errorCode: string): { title: string; description: string } { + const title = getAuthErrorMessage(errorCode, true); + const description = getAuthErrorMessage(errorCode, false); + + return { title, description }; +} + +/** + * Check if an error is a network-related error + * @param error - The error object or message + * @returns True if it's a network error + */ +export function isNetworkError(error: unknown): boolean { + if (error instanceof TypeError && error.message.includes('fetch')) { + return true; + } + + if (typeof error === 'string') { + return /network|connection|fetch|cors/i.test(error); + } + + return false; +} + +/** + * Check if an error should trigger a retry action + * @param errorCode - The error code or message + * @returns True if retry is recommended + */ +export function shouldRetry(errorCode: string): boolean { + const retryableCodes = ['500', '503', '429', 'NETWORK_ERROR', 'TIMEOUT', 'server_error', 'temporarily_unavailable']; + + return retryableCodes.some(code => + errorCode.includes(code) || + errorCode.toUpperCase().includes(code) + ); +} From c2897d7fbecc07ef0da4772161c09a1bb24e7500 Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Thu, 18 Sep 2025 17:46:51 -0700 Subject: [PATCH 2/3] fix: linting and document limit --- .../app/routes/documents_routes.py | 2 +- surfsense_web/app/login/LocalLoginForm.tsx | 21 +- surfsense_web/app/login/page.tsx | 18 +- surfsense_web/app/register/page.tsx | 21 +- surfsense_web/lib/auth-errors.ts | 325 +++++++++--------- 5 files changed, 193 insertions(+), 194 deletions(-) diff --git a/surfsense_backend/app/routes/documents_routes.py b/surfsense_backend/app/routes/documents_routes.py index b75c56663..3ea1e8e61 100644 --- a/surfsense_backend/app/routes/documents_routes.py +++ b/surfsense_backend/app/routes/documents_routes.py @@ -157,7 +157,7 @@ async def create_documents_file_upload( @router.get("/documents/", response_model=list[DocumentRead]) async def read_documents( skip: int = 0, - limit: int = 3000, + limit: int = 300, search_space_id: int | None = None, session: AsyncSession = Depends(get_async_session), user: User = Depends(current_active_user), diff --git a/surfsense_web/app/login/LocalLoginForm.tsx b/surfsense_web/app/login/LocalLoginForm.tsx index 2a4d9239a..b464bea98 100644 --- a/surfsense_web/app/login/LocalLoginForm.tsx +++ b/surfsense_web/app/login/LocalLoginForm.tsx @@ -1,10 +1,10 @@ "use client"; +import { AnimatePresence, motion } from "framer-motion"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; -import { motion, AnimatePresence } from "framer-motion"; import { toast } from "sonner"; -import { getAuthErrorDetails, shouldRetry, isNetworkError } from "@/lib/auth-errors"; +import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; export function LocalLoginForm() { const [username, setUsername] = useState(""); @@ -64,31 +64,30 @@ export function LocalLoginForm() { setTimeout(() => { router.push(`/auth/callback?token=${data.access_token}`); }, 500); - } catch (err) { // Use auth-errors utility to get proper error details let errorCode = "UNKNOWN_ERROR"; - + if (err instanceof Error) { errorCode = err.message; } else if (isNetworkError(err)) { errorCode = "NETWORK_ERROR"; } - + // Get detailed error information from auth-errors utility const errorDetails = getAuthErrorDetails(errorCode); - + // Set persistent error display setErrorTitle(errorDetails.title); setError(errorDetails.description); - + // Show error toast with conditional retry action const toastOptions: any = { id: loadingToast, description: errorDetails.description, duration: 6000, }; - + // Add retry action if the error is retryable if (shouldRetry(errorCode)) { toastOptions.action = { @@ -96,7 +95,7 @@ export function LocalLoginForm() { onClick: () => handleSubmit(e), }; } - + toast.error(errorDetails.title, toastOptions); } finally { setIsLoading(false); @@ -136,9 +135,7 @@ export function LocalLoginForm() {

{errorTitle}

-

- {error} -

+

{error}