diff --git a/surfsense_web/app/(home)/login/LocalLoginForm.tsx b/surfsense_web/app/(home)/login/LocalLoginForm.tsx index cd5f43b48..0157c9faf 100644 --- a/surfsense_web/app/(home)/login/LocalLoginForm.tsx +++ b/surfsense_web/app/(home)/login/LocalLoginForm.tsx @@ -1,4 +1,5 @@ "use client"; +import { useAtom } from "jotai"; import { Eye, EyeOff } from "lucide-react"; import { AnimatePresence, motion } from "motion/react"; import Link from "next/link"; @@ -6,7 +7,9 @@ import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { useEffect, useState } from "react"; import { toast } from "sonner"; +import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms"; import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors"; +import { ValidationError } from "@/lib/error"; export function LocalLoginForm() { const t = useTranslations("auth"); @@ -14,11 +17,16 @@ export function LocalLoginForm() { const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); - const [error, setError] = useState(null); - const [errorTitle, setErrorTitle] = useState(null); - const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState<{ + title: string | null; + message: string | null; + }>({ + title: null, + message: null, + }); const [authType, setAuthType] = useState(null); const router = useRouter(); + const [{ mutateAsync: login, isPending: isLoggingIn }] = useAtom(loginMutationAtom); useEffect(() => { // Get the auth type from environment variables @@ -27,36 +35,17 @@ export function LocalLoginForm() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - setIsLoading(true); - setError(null); // Clear any previous errors - setErrorTitle(null); + setError({ title: null, message: null }); // Clear any previous errors // Show loading toast const loadingToast = toast.loading(tCommon("loading")); 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 || `HTTP ${response.status}`); - } + const data = await login({ + username, + password, + grant_type: "password", + }); // Success toast toast.success(t("login_success"), { @@ -70,6 +59,16 @@ export function LocalLoginForm() { router.push(`/auth/callback?token=${data.access_token}`); }, 500); } catch (err) { + if (err instanceof ValidationError) { + setError({ title: err.name, message: err.message }); + toast.error(err.name, { + id: loadingToast, + description: err.message, + duration: 6000, + }); + return; + } + // Use auth-errors utility to get proper error details let errorCode = "UNKNOWN_ERROR"; @@ -83,8 +82,10 @@ export function LocalLoginForm() { const errorDetails = getAuthErrorDetails(errorCode); // Set persistent error display - setErrorTitle(errorDetails.title); - setError(errorDetails.description); + setError({ + title: errorDetails.title, + message: errorDetails.description, + }); // Show error toast with conditional retry action const toastOptions: any = { @@ -102,8 +103,6 @@ export function LocalLoginForm() { } toast.error(errorDetails.title, toastOptions); - } finally { - setIsLoading(false); } }; @@ -112,7 +111,7 @@ export function LocalLoginForm() {
{/* Error Display */} - {error && errorTitle && ( + {error && error.title && (
-

{errorTitle}

-

{error}

+

{error.title}

+

{error.message}

diff --git a/surfsense_web/contracts/types/auth.types.ts b/surfsense_web/contracts/types/auth.types.ts index 059f18df8..62c128886 100644 --- a/surfsense_web/contracts/types/auth.types.ts +++ b/surfsense_web/contracts/types/auth.types.ts @@ -1,7 +1,7 @@ import { z } from "zod"; export const loginRequest = z.object({ - email: z.string().email("Invalid email address"), + username: z.string(), password: z.string().min(3, "Password must be at least 3 characters"), grant_type: z.string().optional(), }); @@ -11,7 +11,8 @@ export const loginResponse = z.object({ token_type: z.string(), }); -export const registerRequest = loginRequest.omit({ grant_type: true }).extend({ +export const registerRequest = loginRequest.omit({ grant_type: true, username: true }).extend({ + email: z.string().email("Invalid email address"), is_active: z.boolean().optional(), is_superuser: z.boolean().optional(), is_verified: z.boolean().optional(), diff --git a/surfsense_web/lib/apis/auth-api.service.ts b/surfsense_web/lib/apis/auth-api.service.ts index 0689487ba..547b9e5e0 100644 --- a/surfsense_web/lib/apis/auth-api.service.ts +++ b/surfsense_web/lib/apis/auth-api.service.ts @@ -19,11 +19,20 @@ export class AuthApiService { // Format a user frendly error message const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", "); - throw new ValidationError(`Invalid request: ${errorMessage}`, undefined, "VALLIDATION_ERROR"); + throw new ValidationError(`Invalid request: ${errorMessage}`); } - return baseApiService.post(`/auth/jwt/login`, parsedRequest.data, loginResponse, { - contentType: "application/x-www-form-urlencoded", + // Create form data for the API request + const formData = new URLSearchParams(); + formData.append("username", request.username); + formData.append("password", request.password); + formData.append("grant_type", "password"); + + return baseApiService.post(`/auth/jwt/login`, loginResponse, { + body: formData.toString(), + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, }); }; @@ -39,7 +48,9 @@ export class AuthApiService { throw new ValidationError(`Invalid request: ${errorMessage}`); } - return baseApiService.post(`/auth/register`, parsedRequest.data, registerResponse); + return baseApiService.post(`/auth/register`, registerResponse, { + body: parsedRequest.data, + }); }; } diff --git a/surfsense_web/lib/apis/base-api.service.ts b/surfsense_web/lib/apis/base-api.service.ts index 0cd410618..ea7b1cb7d 100644 --- a/surfsense_web/lib/apis/base-api.service.ts +++ b/surfsense_web/lib/apis/base-api.service.ts @@ -20,7 +20,7 @@ export class BaseApiService { bearerToken: string; baseUrl: string; - noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register"]; + noAuthEndpoints: string[] = ["/auth/jwt/login", "/auth/register", "/auth/refresh"]; // Add more endpoints as needed constructor(bearerToken: string, baseUrl: string) { this.bearerToken = bearerToken; @@ -33,7 +33,6 @@ export class BaseApiService { async request( url: string, - body?: any, responseSchema?: z.ZodSchema, options?: RequestOptions ): Promise { @@ -55,25 +54,6 @@ export class BaseApiService { }, }; - // biome-ignore lint/suspicious: Unknown - let requestBody; - - // Serialize body - if (body) { - if (mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === "application/json") { - requestBody = JSON.stringify(body); - } - - if ( - mergedOptions.headers?.["Content-Type"].toLocaleLowerCase() === - "application/x-www-form-urlencoded" - ) { - requestBody = new URLSearchParams(body); - } - - mergedOptions.body = requestBody; - } - if (!this.baseUrl) { throw new AppError("Base URL is not set."); } @@ -161,7 +141,7 @@ export class BaseApiService { responseSchema?: z.ZodSchema, options?: Omit ) { - return this.request(url, undefined, responseSchema, { + return this.request(url, responseSchema, { ...options, method: "GET", }); @@ -169,37 +149,34 @@ export class BaseApiService { async post( url: string, - body?: any, responseSchema?: z.ZodSchema, options?: Omit ) { - return this.request(url, body, responseSchema, { - ...options, + return this.request(url, responseSchema, { method: "POST", + ...options, }); } async put( url: string, - body?: any, responseSchema?: z.ZodSchema, options?: Omit ) { - return this.request(url, body, responseSchema, { - ...options, + return this.request(url, responseSchema, { method: "PUT", + ...options, }); } async delete( url: string, - body?: any, responseSchema?: z.ZodSchema, options?: Omit ) { - return this.request(url, body, responseSchema, { - ...options, + return this.request(url, responseSchema, { method: "DELETE", + ...options, }); } }