mirror of
https://github.com/Nezumi-2711/s3-drive-storage-manage.git
synced 2026-09-22 13:48:31 +00:00
feat: integrate the api for authentication
This commit is contained in:
+3
-3
@@ -1,10 +1,10 @@
|
||||
import { AuthProvider } from "@/contexts/AuthContext"
|
||||
import { QueryProvider } from "@/providers/QueryProvider"
|
||||
import { AppRouter } from "@/router"
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<QueryProvider>
|
||||
<AppRouter />
|
||||
</AuthProvider>
|
||||
</QueryProvider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react"
|
||||
import { Navigate } from "react-router"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
import { useAuth } from "@/features/auth/hooks/useAuth"
|
||||
import { Loader2 } from "lucide-react"
|
||||
|
||||
export function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import React, { useState, useEffect } from "react"
|
||||
import {
|
||||
isAuthenticated as checkIsAuthenticated,
|
||||
login as authLogin,
|
||||
logout as authLogout,
|
||||
type AuthResult,
|
||||
} from "@/lib/auth"
|
||||
import { AuthContext } from "./auth-context-def"
|
||||
|
||||
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Check initial auth state from localStorage
|
||||
setIsAuthenticated(checkIsAuthenticated())
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
const signIn = async (password: string): Promise<AuthResult> => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await authLogin(password)
|
||||
if (result.success) {
|
||||
setIsAuthenticated(true)
|
||||
}
|
||||
return result
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const signOut = () => {
|
||||
authLogout()
|
||||
setIsAuthenticated(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
isAuthenticated,
|
||||
isLoading,
|
||||
signIn,
|
||||
signOut,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export type { AuthContextType } from "./auth-context-def"
|
||||
export { AuthContext } from "./auth-context-def"
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { createContext } from "react"
|
||||
import type { AuthResult } from "@/lib/auth"
|
||||
|
||||
export interface AuthContextType {
|
||||
isAuthenticated: boolean
|
||||
isLoading: boolean
|
||||
signIn: (password: string) => Promise<AuthResult>
|
||||
signOut: () => void
|
||||
}
|
||||
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined)
|
||||
@@ -0,0 +1,54 @@
|
||||
import { apiRequest, ApiError, IS_MOCK_MODE } from "@/lib/api-client"
|
||||
import { hashPassword } from "../lib/hash-password"
|
||||
import { mockLogin, mockLogout, mockVerifySession } from "./auth.mock"
|
||||
import type { LoginResponse, SessionResponse } from "./auth.types"
|
||||
|
||||
/**
|
||||
* Perform login by hashing the password and validating it against the API (or mock).
|
||||
*/
|
||||
export async function login(password: string): Promise<LoginResponse> {
|
||||
if (!password.trim()) {
|
||||
throw new ApiError(400, "Password cannot be empty")
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
if (IS_MOCK_MODE) {
|
||||
return mockLogin(hashedPassword)
|
||||
}
|
||||
|
||||
return apiRequest<LoginResponse>("/auth/login", {
|
||||
method: "POST",
|
||||
body: { passwordHash: hashedPassword },
|
||||
auth: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and verify the current session with the API (or mock).
|
||||
*/
|
||||
export async function fetchSession(signal?: AbortSignal): Promise<SessionResponse> {
|
||||
if (IS_MOCK_MODE) {
|
||||
await mockVerifySession()
|
||||
return { valid: true }
|
||||
}
|
||||
|
||||
return apiRequest<SessionResponse>("/auth/session", {
|
||||
method: "GET",
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform logout with the API (or mock).
|
||||
*/
|
||||
export async function logout(): Promise<void> {
|
||||
if (IS_MOCK_MODE) {
|
||||
await mockLogout()
|
||||
return
|
||||
}
|
||||
|
||||
await apiRequest<void>("/auth/logout", {
|
||||
method: "POST",
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export const authKeys = {
|
||||
all: ["auth"] as const,
|
||||
session: () => [...authKeys.all, "session"] as const,
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { hashPassword, setAuthToken, type AuthResult } from "./auth"
|
||||
import { ApiError } from "@/lib/api-client"
|
||||
import { hashPassword } from "../lib/hash-password"
|
||||
import type { LoginResponse } from "./auth.types"
|
||||
|
||||
// Default mock password if not set in .env
|
||||
const MOCK_ENV_PASSWORD = import.meta.env.VITE_MOCK_PASSWORD || "admin123"
|
||||
@@ -7,7 +9,7 @@ const MOCK_ENV_PASSWORD = import.meta.env.VITE_MOCK_PASSWORD || "admin123"
|
||||
* Mock login function for development when backend is not connected.
|
||||
* Verifies the incoming password hash against the hash of VITE_MOCK_PASSWORD.
|
||||
*/
|
||||
export async function mockLogin(incomingPasswordHash: string): Promise<AuthResult> {
|
||||
export async function mockLogin(incomingPasswordHash: string): Promise<LoginResponse> {
|
||||
// Simulate network latency
|
||||
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||
|
||||
@@ -15,12 +17,22 @@ export async function mockLogin(incomingPasswordHash: string): Promise<AuthResul
|
||||
|
||||
if (incomingPasswordHash === expectedHash) {
|
||||
const mockToken = `mock_token_${Date.now()}_${Math.random().toString(36).substring(2)}`
|
||||
setAuthToken(mockToken)
|
||||
return { success: true, token: mockToken }
|
||||
return { token: mockToken, expiresIn: 86400 }
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Incorrect password. Please try again.",
|
||||
}
|
||||
throw new ApiError(401, "Incorrect password. Please try again.")
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock session verification when backend is not connected.
|
||||
*/
|
||||
export async function mockVerifySession(): Promise<void> {
|
||||
// No-op for mock
|
||||
}
|
||||
|
||||
/**
|
||||
* Mock logout when backend is not connected.
|
||||
*/
|
||||
export async function mockLogout(): Promise<void> {
|
||||
// No-op for mock
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface LoginResponse {
|
||||
token: string
|
||||
expiresIn: number
|
||||
}
|
||||
|
||||
export interface SessionResponse {
|
||||
valid: true
|
||||
}
|
||||
|
||||
export interface ApiErrorBody {
|
||||
message?: string
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useAuthToken } from "@/lib/auth-storage"
|
||||
import { useSession } from "./useSession"
|
||||
import { useLogin } from "./useLogin"
|
||||
import { useLogout } from "./useLogout"
|
||||
|
||||
/**
|
||||
* Facade hook providing authentication status and actions.
|
||||
*/
|
||||
export function useAuth() {
|
||||
const token = useAuthToken()
|
||||
const session = useSession()
|
||||
const loginMutation = useLogin()
|
||||
const logoutMutation = useLogout()
|
||||
|
||||
return {
|
||||
token,
|
||||
isAuthenticated: session.data?.valid === true,
|
||||
isLoading: session.isLoading,
|
||||
signIn: loginMutation.mutateAsync,
|
||||
signOut: logoutMutation.mutateAsync,
|
||||
isSigningIn: loginMutation.isPending,
|
||||
signInError: loginMutation.error,
|
||||
resetSignInError: loginMutation.reset,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { setAuthToken } from "@/lib/auth-storage"
|
||||
import { login } from "../api/auth.api"
|
||||
import { authKeys } from "../api/auth.keys"
|
||||
import type { LoginResponse } from "../api/auth.types"
|
||||
|
||||
/**
|
||||
* Mutation hook for authenticating and logging in.
|
||||
*/
|
||||
export function useLogin() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (password: string) => login(password),
|
||||
onSuccess: (data: LoginResponse) => {
|
||||
setAuthToken(data.token)
|
||||
queryClient.setQueryData(authKeys.session(), { valid: true })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query"
|
||||
import { clearAuthToken } from "@/lib/auth-storage"
|
||||
import { logout } from "../api/auth.api"
|
||||
import { authKeys } from "../api/auth.keys"
|
||||
|
||||
/**
|
||||
* Mutation hook for logging out and clearing credentials.
|
||||
*/
|
||||
export function useLogout() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: () => logout(),
|
||||
onSettled: () => {
|
||||
clearAuthToken()
|
||||
queryClient.removeQueries({ queryKey: authKeys.all })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { queryOptions, useQuery } from "@tanstack/react-query"
|
||||
import { useAuthToken } from "@/lib/auth-storage"
|
||||
import { fetchSession } from "../api/auth.api"
|
||||
import { authKeys } from "../api/auth.keys"
|
||||
|
||||
/**
|
||||
* Query options definition for session verification.
|
||||
*/
|
||||
export function sessionQueryOptions(token: string | null) {
|
||||
return queryOptions({
|
||||
queryKey: authKeys.session(),
|
||||
queryFn: ({ signal }) => fetchSession(signal),
|
||||
enabled: token !== null,
|
||||
staleTime: 5 * 60_000,
|
||||
retry: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to retrieve and subscribe to the active session.
|
||||
*/
|
||||
export function useSession() {
|
||||
const token = useAuthToken()
|
||||
return useQuery(sessionQueryOptions(token))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Hash a plain-text password to a SHA-256 hexadecimal string using the browser's Web Crypto API.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(password)
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { useContext } from "react"
|
||||
import { AuthContext, type AuthContextType } from "@/contexts/auth-context-def"
|
||||
|
||||
export function useAuth(): AuthContextType {
|
||||
const context = useContext(AuthContext)
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { getAuthToken } from "./auth-storage"
|
||||
|
||||
export const API_BASE_URL: string = import.meta.env.VITE_API_URL || ""
|
||||
export const IS_MOCK_MODE: boolean = !API_BASE_URL
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message)
|
||||
this.name = "ApiError"
|
||||
this.status = status
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiRequestOptions extends Omit<RequestInit, "body"> {
|
||||
body?: unknown
|
||||
auth?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Type-safe fetch wrapper with error handling and automatic bearer authentication.
|
||||
*/
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
options: ApiRequestOptions = {}
|
||||
): Promise<T> {
|
||||
const { body, auth = true, headers: customHeaders, ...restOptions } = options
|
||||
|
||||
const headers = new Headers(customHeaders)
|
||||
|
||||
if (auth) {
|
||||
const token = getAuthToken()
|
||||
if (token) {
|
||||
headers.set("Authorization", `Bearer ${token}`)
|
||||
}
|
||||
}
|
||||
|
||||
let requestBody: BodyInit | null | undefined
|
||||
if (body !== undefined) {
|
||||
if (
|
||||
typeof body === "string" ||
|
||||
body instanceof FormData ||
|
||||
body instanceof Blob ||
|
||||
body instanceof ArrayBuffer ||
|
||||
body instanceof URLSearchParams
|
||||
) {
|
||||
requestBody = body as BodyInit
|
||||
} else {
|
||||
headers.set("Content-Type", "application/json")
|
||||
requestBody = JSON.stringify(body)
|
||||
}
|
||||
}
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
...restOptions,
|
||||
headers,
|
||||
body: requestBody,
|
||||
})
|
||||
} catch {
|
||||
throw new ApiError(0, "Failed to connect to authentication service")
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
let errorMessage = "Request failed"
|
||||
try {
|
||||
const data = (await response.json()) as { message?: string }
|
||||
if (data && typeof data.message === "string") {
|
||||
errorMessage = data.message
|
||||
}
|
||||
} catch {
|
||||
errorMessage = response.statusText || `Request failed with status ${response.status}`
|
||||
}
|
||||
throw new ApiError(response.status, errorMessage)
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
if (!text) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text) as T
|
||||
} catch {
|
||||
return text as unknown as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { useSyncExternalStore } from "react"
|
||||
|
||||
export const AUTH_STORAGE_KEY = "s3_drive_storage_auth_token"
|
||||
|
||||
const listeners = new Set<() => void>()
|
||||
|
||||
function emitChange(): void {
|
||||
for (const listener of listeners) {
|
||||
listener()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the auth token from localStorage.
|
||||
*/
|
||||
export function getAuthToken(): string | null {
|
||||
if (typeof window === "undefined") {
|
||||
return null
|
||||
}
|
||||
return localStorage.getItem(AUTH_STORAGE_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the auth token in localStorage and notify listeners.
|
||||
*/
|
||||
export function setAuthToken(token: string): void {
|
||||
localStorage.setItem(AUTH_STORAGE_KEY, token)
|
||||
emitChange()
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the auth token from localStorage and notify listeners.
|
||||
*/
|
||||
export function clearAuthToken(): void {
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY)
|
||||
emitChange()
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener)
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
function getSnapshot(): string | null {
|
||||
return getAuthToken()
|
||||
}
|
||||
|
||||
function getServerSnapshot(): string | null {
|
||||
return null
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
window.addEventListener("storage", (event: StorageEvent) => {
|
||||
if (event.key === AUTH_STORAGE_KEY) {
|
||||
emitChange()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to reactively subscribe to the auth token in localStorage.
|
||||
*/
|
||||
export function useAuthToken(): string | null {
|
||||
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { mockLogin } from "./auth-mock"
|
||||
|
||||
export const AUTH_STORAGE_KEY = "s3_drive_storage_auth_token"
|
||||
export const API_BASE_URL = import.meta.env.VITE_API_URL || ""
|
||||
|
||||
export interface AuthResult {
|
||||
success: boolean
|
||||
token?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash a plain-text password to a SHA-256 hexadecimal string using the browser's Web Crypto API.
|
||||
*/
|
||||
export async function hashPassword(password: string): Promise<string> {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(password)
|
||||
const hashBuffer = await crypto.subtle.digest("SHA-256", data)
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform login by hashing the password and validating it against the API (or mock).
|
||||
*/
|
||||
export async function login(password: string): Promise<AuthResult> {
|
||||
if (!password.trim()) {
|
||||
return { success: false, error: "Password cannot be empty" }
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(password)
|
||||
|
||||
// If no API_BASE_URL configured or in mock mode, use mock authentication
|
||||
if (!API_BASE_URL) {
|
||||
return mockLogin(hashedPassword)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/verify`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ passwordHash: hashedPassword }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json().catch(() => ({}))
|
||||
return {
|
||||
success: false,
|
||||
error: data.message || "Invalid password",
|
||||
}
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const token = data.token || "authenticated"
|
||||
setAuthToken(token)
|
||||
return { success: true, token }
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : "Failed to connect to authentication service",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the auth token in localStorage.
|
||||
*/
|
||||
export function setAuthToken(token: string): void {
|
||||
localStorage.setItem(AUTH_STORAGE_KEY, token)
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the auth token from localStorage.
|
||||
*/
|
||||
export function getAuthToken(): string | null {
|
||||
return localStorage.getItem(AUTH_STORAGE_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the auth token from localStorage.
|
||||
*/
|
||||
export function logout(): void {
|
||||
localStorage.removeItem(AUTH_STORAGE_KEY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is currently authenticated.
|
||||
*/
|
||||
export function isAuthenticated(): boolean {
|
||||
return !!getAuthToken()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { QueryClient, QueryCache } from "@tanstack/react-query"
|
||||
import { ApiError } from "./api-client"
|
||||
import { clearAuthToken } from "./auth-storage"
|
||||
|
||||
/**
|
||||
* Factory function to create a new configured QueryClient instance.
|
||||
*/
|
||||
export function createQueryClient(): QueryClient {
|
||||
let client: QueryClient
|
||||
|
||||
const queryCache = new QueryCache({
|
||||
onError: (error) => {
|
||||
if (error instanceof ApiError && error.status === 401) {
|
||||
clearAuthToken()
|
||||
client?.clear()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
client = new QueryClient({
|
||||
queryCache,
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60_000,
|
||||
gcTime: 5 * 60_000,
|
||||
refetchOnWindowFocus: true,
|
||||
retry: (failureCount, error) => {
|
||||
if (error instanceof ApiError && error.status >= 400 && error.status < 500) {
|
||||
return false
|
||||
}
|
||||
return failureCount < 2
|
||||
},
|
||||
},
|
||||
mutations: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return client
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Cpu,
|
||||
Sparkles,
|
||||
} from "lucide-react"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
import { useAuth } from "@/features/auth/hooks/useAuth"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
@@ -30,8 +30,8 @@ export default function Dashboard() {
|
||||
const { signOut } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleSignOut = () => {
|
||||
signOut()
|
||||
const handleSignOut = async () => {
|
||||
await signOut()
|
||||
navigate("/sign-in", { replace: true })
|
||||
}
|
||||
|
||||
|
||||
+33
-22
@@ -1,7 +1,8 @@
|
||||
import React, { useState } from "react"
|
||||
import { useNavigate, Navigate } from "react-router"
|
||||
import { Eye, EyeOff, Lock, Database, ArrowRight, Loader2, AlertCircle, Sparkles, Shield, Cloud } from "lucide-react"
|
||||
import { useAuth } from "@/hooks/useAuth"
|
||||
import { useAuth } from "@/features/auth/hooks/useAuth"
|
||||
import { ApiError } from "@/lib/api-client"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
@@ -14,13 +15,31 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
|
||||
function getErrorMessage(error: Error | null): string | null {
|
||||
if (!error) return null
|
||||
if (error instanceof ApiError) {
|
||||
if (error.status === 429) {
|
||||
return "Too many failed attempts. Account is temporarily locked out. Please try again later."
|
||||
}
|
||||
if (error.status === 503) {
|
||||
return "Authentication service is unavailable or DASHBOARD_PASSWORD is not configured on backend."
|
||||
}
|
||||
if (error.status === 401) {
|
||||
return error.message || "Incorrect password. Please try again."
|
||||
}
|
||||
if (error.status === 0) {
|
||||
return error.message || "Failed to connect to authentication service"
|
||||
}
|
||||
return error.message
|
||||
}
|
||||
return error.message || "An unexpected error occurred. Please try again."
|
||||
}
|
||||
|
||||
export default function SignIn() {
|
||||
const [password, setPassword] = useState("")
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
|
||||
const { signIn, isAuthenticated, isLoading } = useAuth()
|
||||
const { signIn, isAuthenticated, isLoading, isSigningIn, signInError, resetSignInError } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// If already authenticated and not in loading state, redirect to dashboard
|
||||
@@ -28,27 +47,19 @@ export default function SignIn() {
|
||||
return <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
const error = getErrorMessage(signInError)
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!password.trim()) {
|
||||
setError("Please enter your password")
|
||||
if (!password.trim() || isSigningIn) {
|
||||
return
|
||||
}
|
||||
|
||||
setError(null)
|
||||
setSubmitting(true)
|
||||
|
||||
try {
|
||||
const result = await signIn(password)
|
||||
if (result.success) {
|
||||
navigate("/", { replace: true })
|
||||
} else {
|
||||
setError(result.error || "Authentication failed")
|
||||
}
|
||||
await signIn(password)
|
||||
navigate("/", { replace: true })
|
||||
} catch {
|
||||
setError("An unexpected error occurred. Please try again.")
|
||||
} finally {
|
||||
setSubmitting(false)
|
||||
// Error handled by mutation state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,11 +140,11 @@ export default function SignIn() {
|
||||
value={password}
|
||||
onChange={(e) => {
|
||||
setPassword(e.target.value)
|
||||
if (error) setError(null)
|
||||
if (signInError) resetSignInError()
|
||||
}}
|
||||
placeholder="Enter management password"
|
||||
className="pl-10 pr-10 h-11 text-sm bg-background/80 border-border/80 rounded-xl focus-visible:ring-2 focus-visible:ring-blue-500/40 focus-visible:border-blue-500 transition-all"
|
||||
disabled={submitting}
|
||||
disabled={isSigningIn}
|
||||
autoComplete="current-password"
|
||||
autoFocus
|
||||
/>
|
||||
@@ -158,9 +169,9 @@ export default function SignIn() {
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-11 font-semibold rounded-xl text-sm"
|
||||
disabled={submitting || !password.trim()}
|
||||
disabled={isSigningIn || !password.trim()}
|
||||
>
|
||||
{submitting ? (
|
||||
{isSigningIn ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
Verifying Credentials...
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import React, { useState, lazy, Suspense } from "react"
|
||||
import { QueryClientProvider } from "@tanstack/react-query"
|
||||
import { createQueryClient } from "@/lib/query-client"
|
||||
|
||||
const ReactQueryDevtools = import.meta.env.DEV
|
||||
? lazy(() =>
|
||||
import("@tanstack/react-query-devtools").then((m) => ({
|
||||
default: m.ReactQueryDevtools,
|
||||
}))
|
||||
)
|
||||
: () => null
|
||||
|
||||
export interface QueryProviderProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function QueryProvider({ children }: QueryProviderProps) {
|
||||
const [queryClient] = useState(() => createQueryClient())
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<Suspense fallback={null}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</Suspense>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user