feat(auth): add password visibility toggle and remove the unused Organization field

Part 1 — show/hide password: an eye/eye-off button inside the login password input switches its type between password and text. It sits on the right (the input is LTR) with pr-10 clearance so dots never overlap the icon; type=button so a click cannot submit the form, aria-label Show/Hide password, aria-pressed + aria-controls, plus a Persian tooltip.

Part 2 — Organization field removed end-to-end after the audit proved it was a no-op: LoginPage input/state/payload spread (main + dev-mode admin login), api.ts payload type, backend LoginRequest.organization (declared but never forwarded to Casdoor — a bogus org value still logged in successfully), and the casdoor-auth.md flow diagram. The organization actually used remains CASDOOR_ORGANIZATION=inventory for /users/sync.

Verified: api + frontend images rebuilt and redeployed; login works with {username, password}; the served LoginPage chunk contains the toggle and zero org-field leftovers.
This commit is contained in:
2026-09-15 16:29:54 +03:30
parent e7f465f74f
commit 593c149478
4 changed files with 31 additions and 32 deletions
-1
View File
@@ -31,7 +31,6 @@ router = APIRouter(prefix="/auth", tags=["auth"])
class LoginRequest(BaseModel):
username: str
password: str
organization: str | None = None # optional override; defaults to CASDOOR_ORGANIZATION
@router.post("/login")
+1 -1
View File
@@ -35,7 +35,7 @@ to re-run).
## 2. Login flow (in-page, no redirect)
```
Browser ──► POST /api/v1/auth/login {username, password, organization?}
Browser ──► POST /api/v1/auth/login {username, password}
│
▼
Backend ──► POST http://casdoor:8000/api/login/oauth/access_token
-1
View File
@@ -1460,7 +1460,6 @@ export const apiV26 = {
login: async (body: {
username: string;
password: string;
organization?: string;
}): Promise<{ access_token: string; expires_in: number | null; user: import("../store/auth").AuthUser }> => {
const res = await request<{
data: {
+30 -29
View File
@@ -1,14 +1,14 @@
import { useEffect, useMemo, useState } from "react";
import { useNavigate } from "react-router-dom";
import { toast } from "sonner";
import { ClipboardList, Loader2, Lock, ShieldCheck, Wrench, X } from "lucide-react";
import { ClipboardList, Eye, EyeOff, Loader2, Lock, ShieldCheck, Wrench, X } from "lucide-react";
import { api, apiV26 } from "../../lib/api";
import { useAuthStore } from "../../store/auth";
import type { Project } from "../../lib/types";
// ============================================================
// ورود به سامانه — Casdoor درون‌صفحه‌ای (بدون ریدایرکت)
// فرم اصلی: Username / Password / Organization (اختیاری) →
// فرم اصلی: Username / Password →
// POST /api/v1/auth/login (بک‌اند با grant_type=password از
// Casdoor توکن می‌گیرد و نقش را از جدول users خودمان برمی‌گرداند)
// حالت توسعه (فقط ادمین — سوپر یوزر) — چرخه‌ی ۳ مرحله‌ای:
@@ -36,7 +36,7 @@ export function LoginPage() {
// ---- فرم اصلی (Casdoor) ----
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [organization, setOrganization] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
// ---- پنل حالت توسعه (۳ مرحله) ----
@@ -71,7 +71,6 @@ export function LoginPage() {
const { access_token, user } = await apiV26.auth.login({
username: username.trim(),
password,
...(organization.trim() ? { organization: organization.trim() } : {}),
});
if (!user.role) {
toast.error("نقش شما هنوز در سیستم تعیین نشده است — با مدیر سیستم تماس بگیرید");
@@ -101,7 +100,6 @@ export function LoginPage() {
const { user: adminUser } = await apiV26.auth.login({
username: devAdminUser.trim(),
password: devAdminPass,
...(organization.trim() ? { organization: organization.trim() } : {}),
});
if (adminUser.role !== "admin") {
toast.error("حالت توسعه فقط برای ادمین (سوپر یوزر) در دسترس است");
@@ -298,30 +296,33 @@ export function LoginPage() {
<label className="mb-1 block text-[12px] font-semibold text-muted" htmlFor="login-password">
رمز عبور
</label>
<input
id="login-password"
type="password"
autoComplete="current-password"
dir="ltr"
className="h-10 w-full rounded-lg border border-line bg-panel px-3 text-left text-sm shadow-sm placeholder:text-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
placeholder="••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<div className="field">
<label className="mb-1 block text-[12px] font-semibold text-muted" htmlFor="login-org">
سازمان (اختیاری)
</label>
<input
id="login-org"
type="text"
dir="ltr"
className="h-10 w-full rounded-lg border border-line bg-panel px-3 text-left text-sm shadow-sm placeholder:text-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
placeholder="inventory"
value={organization}
onChange={(e) => setOrganization(e.target.value)}
/>
<div className="relative">
<input
id="login-password"
type={showPassword ? "text" : "password"}
autoComplete="current-password"
dir="ltr"
className="h-10 w-full rounded-lg border border-line bg-panel px-3 pr-10 text-left text-sm shadow-sm placeholder:text-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
placeholder="••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
type="button"
onClick={() => setShowPassword((v) => !v)}
aria-label={showPassword ? "Hide password" : "Show password"}
aria-pressed={showPassword}
aria-controls="login-password"
title={showPassword ? "پنهان کردن رمز" : "نمایش رمز"}
className="absolute inset-y-0 right-0 flex cursor-pointer items-center rounded-lg px-3 text-muted transition-colors hover:text-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
>
{showPassword ? (
<EyeOff className="h-4 w-4" aria-hidden="true" />
) : (
<Eye className="h-4 w-4" aria-hidden="true" />
)}
</button>
</div>
</div>
<button
type="submit"