feat: normalize email login and improve theme handling
CI / backend (push) Failing after 2s
CI / frontend (push) Failing after 27s

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
This commit is contained in:
2026-07-09 12:23:28 +02:00
parent 4fd7e5acbf
commit dea27b5459
6 changed files with 27 additions and 15 deletions
+2 -2
View File
@@ -16,7 +16,8 @@ router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/login", response_model=TokenPair) @router.post("/login", response_model=TokenPair)
def login(payload: LoginRequest, db: Session = Depends(get_db)) -> 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 not user or not verify_password(payload.password, user.password_hash):
if user: if user:
user.failed_login_attempts += 1 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]: 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) write_audit(db, action="logout", object_type="user", object_id=user.id, user_id=user.id)
return {"status": "ok", "at": datetime.utcnow().isoformat()} return {"status": "ok", "at": datetime.utcnow().isoformat()}
+3 -3
View File
@@ -1,7 +1,7 @@
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from pydantic import BaseModel, ConfigDict, EmailStr, Field from pydantic import BaseModel, ConfigDict, Field
class OrmModel(BaseModel): class OrmModel(BaseModel):
@@ -15,13 +15,13 @@ class TokenPair(BaseModel):
class LoginRequest(BaseModel): class LoginRequest(BaseModel):
email: EmailStr email: str
password: str password: str
class UserRead(OrmModel): class UserRead(OrmModel):
id: str id: str
email: EmailStr email: str
display_name: str display_name: str
is_active: bool is_active: bool
+8 -1
View File
@@ -1,4 +1,5 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useEffect } from "react";
import { BrowserRouter, Route, Routes } from "react-router-dom"; import { BrowserRouter, Route, Routes } from "react-router-dom";
import { Layout } from "./components/Layout"; import { Layout } from "./components/Layout";
@@ -7,10 +8,17 @@ import { FirewallPreview } from "./pages/FirewallPreview";
import { ListPage } from "./pages/ListPage"; import { ListPage } from "./pages/ListPage";
import { Login } from "./pages/Login"; import { Login } from "./pages/Login";
import { PolicyDesigner } from "./pages/PolicyDesigner"; import { PolicyDesigner } from "./pages/PolicyDesigner";
import { useTheme } from "./stores/theme";
const queryClient = new QueryClient(); const queryClient = new QueryClient();
export function App() { export function App() {
const dark = useTheme((state) => state.dark);
useEffect(() => {
document.documentElement.classList.toggle("dark", dark);
}, [dark]);
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<BrowserRouter> <BrowserRouter>
@@ -38,4 +46,3 @@ export function App() {
</QueryClientProvider> </QueryClientProvider>
); );
} }
-5
View File
@@ -45,10 +45,6 @@ export function Layout() {
const navigate = useNavigate(); const navigate = useNavigate();
const { dark, toggle } = useTheme(); const { dark, toggle } = useTheme();
useEffect(() => {
document.documentElement.classList.toggle("dark", dark);
}, [dark]);
useEffect(() => { useEffect(() => {
if (!token()) navigate("/login"); if (!token()) navigate("/login");
}, [navigate]); }, [navigate]);
@@ -96,4 +92,3 @@ export function Layout() {
</div> </div>
); );
} }
+11 -2
View File
@@ -1,14 +1,16 @@
import { FormEvent, useState } from "react"; import { FormEvent, useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ShieldCheck } from "lucide-react"; import { Moon, ShieldCheck, Sun } from "lucide-react";
import { login } from "../api/client"; import { login } from "../api/client";
import { useTheme } from "../stores/theme";
export function Login() { export function Login() {
const navigate = useNavigate(); const navigate = useNavigate();
const [email, setEmail] = useState("admin@nexafabric.local"); const [email, setEmail] = useState("admin@nexafabric.local");
const [password, setPassword] = useState("ChangeMe_UseEnvInstead"); const [password, setPassword] = useState("ChangeMe_UseEnvInstead");
const [error, setError] = useState(""); const [error, setError] = useState("");
const { dark, toggle } = useTheme();
async function submit(event: FormEvent) { async function submit(event: FormEvent) {
event.preventDefault(); event.preventDefault();
@@ -23,6 +25,14 @@ export function Login() {
return ( return (
<div className="grid min-h-screen place-items-center bg-canvas px-4"> <div className="grid min-h-screen place-items-center bg-canvas px-4">
<button
className="fixed right-4 top-4 rounded-md border border-border bg-panel p-2 text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800"
onClick={toggle}
type="button"
aria-label="Toggle theme"
>
{dark ? <Sun size={18} /> : <Moon size={18} />}
</button>
<form onSubmit={submit} className="w-full max-w-sm rounded-md border border-border bg-panel p-6 shadow-sm"> <form onSubmit={submit} className="w-full max-w-sm rounded-md border border-border bg-panel p-6 shadow-sm">
<div className="mb-6 flex items-center gap-3"> <div className="mb-6 flex items-center gap-3">
<div className="rounded-md bg-accent p-2 text-white"> <div className="rounded-md bg-accent p-2 text-white">
@@ -47,4 +57,3 @@ export function Login() {
</div> </div>
); );
} }
+3 -2
View File
@@ -5,8 +5,10 @@ type ThemeState = {
toggle: () => void; toggle: () => void;
}; };
const storedTheme = localStorage.getItem("nexafabric.theme");
export const useTheme = create<ThemeState>((set) => ({ export const useTheme = create<ThemeState>((set) => ({
dark: localStorage.getItem("nexafabric.theme") === "dark", dark: storedTheme ? storedTheme === "dark" : true,
toggle: () => toggle: () =>
set((state) => { set((state) => {
const dark = !state.dark; const dark = !state.dark;
@@ -14,4 +16,3 @@ export const useTheme = create<ThemeState>((set) => ({
return { dark }; return { dark };
}), }),
})); }));