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
This commit is contained in:
@@ -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()}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 };
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user