mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
update login form
This commit is contained in:
parent
41a938cec0
commit
b35a5aa589
4 changed files with 67 additions and 80 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { useAtom } from "jotai";
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -6,7 +7,9 @@ import { useRouter } from "next/navigation";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { loginMutationAtom } from "@/atoms/auth/auth-mutation.atoms";
|
||||||
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
|
import { getAuthErrorDetails, isNetworkError, shouldRetry } from "@/lib/auth-errors";
|
||||||
|
import { ValidationError } from "@/lib/error";
|
||||||
|
|
||||||
export function LocalLoginForm() {
|
export function LocalLoginForm() {
|
||||||
const t = useTranslations("auth");
|
const t = useTranslations("auth");
|
||||||
|
|
@ -14,11 +17,16 @@ export function LocalLoginForm() {
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<{
|
||||||
const [errorTitle, setErrorTitle] = useState<string | null>(null);
|
title: string | null;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
message: string | null;
|
||||||
|
}>({
|
||||||
|
title: null,
|
||||||
|
message: null,
|
||||||
|
});
|
||||||
const [authType, setAuthType] = useState<string | null>(null);
|
const [authType, setAuthType] = useState<string | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const [{ mutateAsync: login, isPending: isLoggingIn }] = useAtom(loginMutationAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Get the auth type from environment variables
|
// Get the auth type from environment variables
|
||||||
|
|
@ -27,36 +35,17 @@ export function LocalLoginForm() {
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setError({ title: null, message: null }); // Clear any previous errors
|
||||||
setError(null); // Clear any previous errors
|
|
||||||
setErrorTitle(null);
|
|
||||||
|
|
||||||
// Show loading toast
|
// Show loading toast
|
||||||
const loadingToast = toast.loading(tCommon("loading"));
|
const loadingToast = toast.loading(tCommon("loading"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create form data for the API request
|
const data = await login({
|
||||||
const formData = new URLSearchParams();
|
username,
|
||||||
formData.append("username", username);
|
password,
|
||||||
formData.append("password", password);
|
grant_type: "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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success toast
|
// Success toast
|
||||||
toast.success(t("login_success"), {
|
toast.success(t("login_success"), {
|
||||||
|
|
@ -70,6 +59,16 @@ export function LocalLoginForm() {
|
||||||
router.push(`/auth/callback?token=${data.access_token}`);
|
router.push(`/auth/callback?token=${data.access_token}`);
|
||||||
}, 500);
|
}, 500);
|
||||||
} catch (err) {
|
} 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
|
// Use auth-errors utility to get proper error details
|
||||||
let errorCode = "UNKNOWN_ERROR";
|
let errorCode = "UNKNOWN_ERROR";
|
||||||
|
|
||||||
|
|
@ -83,8 +82,10 @@ export function LocalLoginForm() {
|
||||||
const errorDetails = getAuthErrorDetails(errorCode);
|
const errorDetails = getAuthErrorDetails(errorCode);
|
||||||
|
|
||||||
// Set persistent error display
|
// Set persistent error display
|
||||||
setErrorTitle(errorDetails.title);
|
setError({
|
||||||
setError(errorDetails.description);
|
title: errorDetails.title,
|
||||||
|
message: errorDetails.description,
|
||||||
|
});
|
||||||
|
|
||||||
// Show error toast with conditional retry action
|
// Show error toast with conditional retry action
|
||||||
const toastOptions: any = {
|
const toastOptions: any = {
|
||||||
|
|
@ -102,8 +103,6 @@ export function LocalLoginForm() {
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.error(errorDetails.title, toastOptions);
|
toast.error(errorDetails.title, toastOptions);
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -112,7 +111,7 @@ export function LocalLoginForm() {
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{/* Error Display */}
|
{/* Error Display */}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{error && errorTitle && (
|
{error && error.title && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: -10, scale: 0.95 }}
|
initial={{ opacity: 0, y: -10, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
|
|
@ -139,13 +138,12 @@ export function LocalLoginForm() {
|
||||||
<line x1="9" y1="9" x2="15" y2="15" />
|
<line x1="9" y1="9" x2="15" y2="15" />
|
||||||
</svg>
|
</svg>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-sm font-semibold mb-1">{errorTitle}</p>
|
<p className="text-sm font-semibold mb-1">{error.title}</p>
|
||||||
<p className="text-sm text-red-700 dark:text-red-300">{error}</p>
|
<p className="text-sm text-red-700 dark:text-red-300">{error.message}</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setError(null);
|
setError({ title: null, message: null });
|
||||||
setErrorTitle(null);
|
|
||||||
}}
|
}}
|
||||||
className="flex-shrink-0 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200 transition-colors"
|
className="flex-shrink-0 text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-200 transition-colors"
|
||||||
aria-label="Dismiss error"
|
aria-label="Dismiss error"
|
||||||
|
|
@ -186,11 +184,11 @@ export function LocalLoginForm() {
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
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 ${
|
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
|
error.title
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
|
? "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"
|
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
|
||||||
}`}
|
}`}
|
||||||
disabled={isLoading}
|
disabled={isLoggingIn}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -209,11 +207,11 @@ export function LocalLoginForm() {
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className={`mt-1 block w-full rounded-md border pr-10 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 ${
|
className={`mt-1 block w-full rounded-md border pr-10 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
|
error.title
|
||||||
? "border-red-300 focus:border-red-500 focus:ring-red-500 dark:border-red-700"
|
? "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"
|
: "border-gray-300 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-700"
|
||||||
}`}
|
}`}
|
||||||
disabled={isLoading}
|
disabled={isLoggingIn}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -228,10 +226,10 @@ export function LocalLoginForm() {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={isLoading}
|
disabled={isLoggingIn}
|
||||||
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 transition-colors"
|
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 transition-colors"
|
||||||
>
|
>
|
||||||
{isLoading ? tCommon("loading") : t("sign_in")}
|
{isLoggingIn ? tCommon("loading") : t("sign_in")}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const loginRequest = z.object({
|
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"),
|
password: z.string().min(3, "Password must be at least 3 characters"),
|
||||||
grant_type: z.string().optional(),
|
grant_type: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
@ -11,7 +11,8 @@ export const loginResponse = z.object({
|
||||||
token_type: z.string(),
|
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_active: z.boolean().optional(),
|
||||||
is_superuser: z.boolean().optional(),
|
is_superuser: z.boolean().optional(),
|
||||||
is_verified: z.boolean().optional(),
|
is_verified: z.boolean().optional(),
|
||||||
|
|
|
||||||
|
|
@ -19,11 +19,20 @@ export class AuthApiService {
|
||||||
|
|
||||||
// Format a user frendly error message
|
// Format a user frendly error message
|
||||||
const errorMessage = parsedRequest.error.errors.map((err) => err.message).join(", ");
|
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, {
|
// Create form data for the API request
|
||||||
contentType: "application/x-www-form-urlencoded",
|
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}`);
|
throw new ValidationError(`Invalid request: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return baseApiService.post(`/auth/register`, parsedRequest.data, registerResponse);
|
return baseApiService.post(`/auth/register`, registerResponse, {
|
||||||
|
body: parsedRequest.data,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export class BaseApiService {
|
||||||
bearerToken: string;
|
bearerToken: string;
|
||||||
baseUrl: 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) {
|
constructor(bearerToken: string, baseUrl: string) {
|
||||||
this.bearerToken = bearerToken;
|
this.bearerToken = bearerToken;
|
||||||
|
|
@ -33,7 +33,6 @@ export class BaseApiService {
|
||||||
|
|
||||||
async request<T>(
|
async request<T>(
|
||||||
url: string,
|
url: string,
|
||||||
body?: any,
|
|
||||||
responseSchema?: z.ZodSchema<T>,
|
responseSchema?: z.ZodSchema<T>,
|
||||||
options?: RequestOptions
|
options?: RequestOptions
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
|
|
@ -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) {
|
if (!this.baseUrl) {
|
||||||
throw new AppError("Base URL is not set.");
|
throw new AppError("Base URL is not set.");
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +141,7 @@ export class BaseApiService {
|
||||||
responseSchema?: z.ZodSchema<T>,
|
responseSchema?: z.ZodSchema<T>,
|
||||||
options?: Omit<RequestOptions, "method">
|
options?: Omit<RequestOptions, "method">
|
||||||
) {
|
) {
|
||||||
return this.request(url, undefined, responseSchema, {
|
return this.request(url, responseSchema, {
|
||||||
...options,
|
...options,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
|
|
@ -169,37 +149,34 @@ export class BaseApiService {
|
||||||
|
|
||||||
async post<T>(
|
async post<T>(
|
||||||
url: string,
|
url: string,
|
||||||
body?: any,
|
|
||||||
responseSchema?: z.ZodSchema<T>,
|
responseSchema?: z.ZodSchema<T>,
|
||||||
options?: Omit<RequestOptions, "method">
|
options?: Omit<RequestOptions, "method">
|
||||||
) {
|
) {
|
||||||
return this.request(url, body, responseSchema, {
|
return this.request(url, responseSchema, {
|
||||||
...options,
|
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async put<T>(
|
async put<T>(
|
||||||
url: string,
|
url: string,
|
||||||
body?: any,
|
|
||||||
responseSchema?: z.ZodSchema<T>,
|
responseSchema?: z.ZodSchema<T>,
|
||||||
options?: Omit<RequestOptions, "method">
|
options?: Omit<RequestOptions, "method">
|
||||||
) {
|
) {
|
||||||
return this.request(url, body, responseSchema, {
|
return this.request(url, responseSchema, {
|
||||||
...options,
|
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete<T>(
|
async delete<T>(
|
||||||
url: string,
|
url: string,
|
||||||
body?: any,
|
|
||||||
responseSchema?: z.ZodSchema<T>,
|
responseSchema?: z.ZodSchema<T>,
|
||||||
options?: Omit<RequestOptions, "method">
|
options?: Omit<RequestOptions, "method">
|
||||||
) {
|
) {
|
||||||
return this.request(url, body, responseSchema, {
|
return this.request(url, responseSchema, {
|
||||||
...options,
|
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
...options,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue