From dea27b5459b71c425d56a127cb974acf8c77e295 Mon Sep 17 00:00:00 2001 From: nessi Date: Thu, 9 Jul 2026 12:23:28 +0200 Subject: [PATCH] feat: normalize email login and improve theme handling Normalize email input by stripping whitespace and converting to lowercase in login endpoint, remove EmailStr validation in favor of plain string type, move dark mode class toggle from Layout to App component for better initialization, add theme toggle button to login page, and set dark mode as default theme when no preference is stored --- backend/app/api/v1/auth.py | 4 ++-- backend/app/schemas/domain.py | 6 +++--- frontend/src/App.tsx | 9 ++++++++- frontend/src/components/Layout.tsx | 5 ----- frontend/src/pages/Login.tsx | 13 +++++++++++-- frontend/src/stores/theme.ts | 5 +++-- 6 files changed, 27 insertions(+), 15 deletions(-) diff --git a/backend/app/api/v1/auth.py b/backend/app/api/v1/auth.py index e34ee71..e5784a1 100644 --- a/backend/app/api/v1/auth.py +++ b/backend/app/api/v1/auth.py @@ -16,7 +16,8 @@ router = APIRouter(prefix="/auth", tags=["auth"]) @router.post("/login", response_model=TokenPair) def login(payload: LoginRequest, db: Session = Depends(get_db)) -> TokenPair: - user = db.scalar(select(User).where(User.email == payload.email)) + email = payload.email.strip().lower() + user = db.scalar(select(User).where(User.email == email)) if not user or not verify_password(payload.password, user.password_hash): if user: user.failed_login_attempts += 1 @@ -41,4 +42,3 @@ def me(user: CurrentUser) -> User: def logout(user: CurrentUser, db: Session = Depends(get_db)) -> dict[str, str]: write_audit(db, action="logout", object_type="user", object_id=user.id, user_id=user.id) return {"status": "ok", "at": datetime.utcnow().isoformat()} - diff --git a/backend/app/schemas/domain.py b/backend/app/schemas/domain.py index e5762a1..f47ac2e 100644 --- a/backend/app/schemas/domain.py +++ b/backend/app/schemas/domain.py @@ -1,7 +1,7 @@ from datetime import datetime from typing import Any -from pydantic import BaseModel, ConfigDict, EmailStr, Field +from pydantic import BaseModel, ConfigDict, Field class OrmModel(BaseModel): @@ -15,13 +15,13 @@ class TokenPair(BaseModel): class LoginRequest(BaseModel): - email: EmailStr + email: str password: str class UserRead(OrmModel): id: str - email: EmailStr + email: str display_name: str is_active: bool diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6cb5464..c883352 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useEffect } from "react"; import { BrowserRouter, Route, Routes } from "react-router-dom"; import { Layout } from "./components/Layout"; @@ -7,10 +8,17 @@ import { FirewallPreview } from "./pages/FirewallPreview"; import { ListPage } from "./pages/ListPage"; import { Login } from "./pages/Login"; import { PolicyDesigner } from "./pages/PolicyDesigner"; +import { useTheme } from "./stores/theme"; const queryClient = new QueryClient(); export function App() { + const dark = useTheme((state) => state.dark); + + useEffect(() => { + document.documentElement.classList.toggle("dark", dark); + }, [dark]); + return ( @@ -38,4 +46,3 @@ export function App() { ); } - diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index f1f038f..3472ebc 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -45,10 +45,6 @@ export function Layout() { const navigate = useNavigate(); const { dark, toggle } = useTheme(); - useEffect(() => { - document.documentElement.classList.toggle("dark", dark); - }, [dark]); - useEffect(() => { if (!token()) navigate("/login"); }, [navigate]); @@ -96,4 +92,3 @@ export function Layout() { ); } - diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx index 31e6029..d2ba941 100644 --- a/frontend/src/pages/Login.tsx +++ b/frontend/src/pages/Login.tsx @@ -1,14 +1,16 @@ import { FormEvent, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { ShieldCheck } from "lucide-react"; +import { Moon, ShieldCheck, Sun } from "lucide-react"; import { login } from "../api/client"; +import { useTheme } from "../stores/theme"; export function Login() { const navigate = useNavigate(); const [email, setEmail] = useState("admin@nexafabric.local"); const [password, setPassword] = useState("ChangeMe_UseEnvInstead"); const [error, setError] = useState(""); + const { dark, toggle } = useTheme(); async function submit(event: FormEvent) { event.preventDefault(); @@ -23,6 +25,14 @@ export function Login() { return (
+
@@ -47,4 +57,3 @@ export function Login() {
); } - diff --git a/frontend/src/stores/theme.ts b/frontend/src/stores/theme.ts index 31e3db0..e1ee999 100644 --- a/frontend/src/stores/theme.ts +++ b/frontend/src/stores/theme.ts @@ -5,8 +5,10 @@ type ThemeState = { toggle: () => void; }; +const storedTheme = localStorage.getItem("nexafabric.theme"); + export const useTheme = create((set) => ({ - dark: localStorage.getItem("nexafabric.theme") === "dark", + dark: storedTheme ? storedTheme === "dark" : true, toggle: () => set((state) => { const dark = !state.dark; @@ -14,4 +16,3 @@ export const useTheme = create((set) => ({ return { dark }; }), })); -