feat: simple login page with mtdemne / 09128121946 credentials and reset auto-session

This commit is contained in:
alireza 2026-08-27 11:32:09 +03:30
parent 719f18f553
commit 23dfc84a84
2 changed files with 124 additions and 186 deletions

View File

@ -1,226 +1,147 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { authService } from '../services/authService'; import { authService } from '../services/authService';
import { ForgotPasswordModal } from '../components/modals/ForgotPasswordModal';
import { toPersianDigits } from '../utils/persianNumbers'; import { toPersianDigits } from '../utils/persianNumbers';
import { HORIZON_START, HORIZON_END } from '../utils/horizon'; import { HORIZON_START, HORIZON_END } from '../utils/horizon';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { Compass, AlertCircle, Loader2 } from 'lucide-react'; import { Compass, AlertCircle, Loader2, KeyRound, User, Lock } from 'lucide-react';
interface LoginProps { interface LoginProps {
onLoginSuccess: () => void; onLoginSuccess: () => void;
} }
export const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => { export const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
const [username, setUsername] = useState(''); const [username, setUsername] = useState('mtdemne');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('09128121946');
const [mobileNumber, setMobileNumber] = useState('');
const [otpCode, setOtpCode] = useState('');
const [otpSent, setOtpSent] = useState(false);
const [simulatedCode, setSimulatedCode] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState(''); const [errorMsg, setErrorMsg] = useState('');
const [showForgot, setShowForgot] = useState(false);
const run = async (fn: () => Promise<void>, fallbackError: string) => { const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
if (!username.trim()) {
setErrorMsg('لطفاً نام کاربری را وارد کنید');
return;
}
setLoading(true); setLoading(true);
setErrorMsg(''); setErrorMsg('');
try { try {
await fn(); await authService.loginWithCredentials(username, password);
onLoginSuccess();
} catch (err) { } catch (err) {
setErrorMsg(err instanceof Error ? err.message : fallbackError); setErrorMsg(err instanceof Error ? err.message : 'نام کاربری یا رمز عبور اشتباه است');
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const handlePasswordLogin = (e: React.FormEvent) => {
e.preventDefault();
if (!username.trim()) return;
run(async () => {
await authService.loginWithCredentials(username, password);
onLoginSuccess();
}, 'خطا در ورود');
};
const handleSendOtp = (e: React.FormEvent) => {
e.preventDefault();
if (!mobileNumber.trim()) return;
run(async () => {
const res = await authService.sendOtp(mobileNumber);
setSimulatedCode(res.simulatedOtp);
setOtpSent(true);
}, 'خطا در ارسال کد');
};
const handleVerifyOtp = (e: React.FormEvent) => {
e.preventDefault();
if (!otpCode.trim()) return;
run(async () => {
await authService.verifyOtp(otpCode);
onLoginSuccess();
}, 'کد وارد شده نامعتبر است');
};
return ( return (
<div className="flex min-h-svh w-full items-center justify-center bg-background p-4"> <div className="flex min-h-svh w-full items-center justify-center bg-background p-4">
<div className="w-full max-w-sm"> <div className="w-full max-w-sm">
{/* Brand */} {/* Brand Header */}
<div className="mb-6 flex flex-col items-center text-center"> <div className="mb-6 flex flex-col items-center text-center">
<span className="mb-3 flex size-12 items-center justify-center rounded-xl bg-primary text-primary-foreground"> <span className="mb-3 flex size-12 items-center justify-center rounded-xl bg-primary text-primary-foreground shadow-sm">
<Compass className="size-6" strokeWidth={2} /> <Compass className="size-6" strokeWidth={2} />
</span> </span>
<h1 className="text-lg font-bold">رادار آینده دانشگاه اصفهان</h1> <h1 className="text-xl font-bold tracking-tight text-foreground">
رادار آینده دانشگاه اصفهان
</h1>
<p className="tnum mt-1 text-[12px] text-muted-foreground"> <p className="tnum mt-1 text-[12px] text-muted-foreground">
افق {toPersianDigits(HORIZON_START)} تا {toPersianDigits(HORIZON_END)} سامانه هوشمندی راهبردی افق {toPersianDigits(HORIZON_START)} تا {toPersianDigits(HORIZON_END)}
</p> </p>
</div> </div>
<Card data-card> {/* Login Card */}
<CardContent className="p-5"> <Card data-card className="border shadow-md">
<Tabs <CardHeader className="pb-4">
defaultValue="password" <CardTitle className="text-[16px] font-bold">ورود به سامانه</CardTitle>
onValueChange={() => setErrorMsg('')} <CardDescription className="text-[12px]">
className="gap-4" جهت دسترسی به داشبورد و رادار هوشمندی، مشخصات کاربری خود را وارد فرمایید.
> </CardDescription>
<TabsList className="w-full"> </CardHeader>
<TabsTrigger value="password" className="flex-1">
<CardContent className="space-y-4">
{errorMsg && (
<Alert variant="destructive" className="py-2.5">
<AlertCircle className="size-4" />
<AlertDescription className="text-[12px]">{errorMsg}</AlertDescription>
</Alert>
)}
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="username" className="text-[12.5px] font-medium">
نام کاربری
</Label>
<div className="relative">
<Input
id="username"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="mtdemne"
autoComplete="username"
dir="ltr"
className="pl-9 text-left font-mono text-[13px]"
/>
<User className="absolute left-3 top-2.5 size-4 text-muted-foreground" />
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="password" className="text-[12.5px] font-medium">
رمز عبور رمز عبور
</TabsTrigger> </Label>
<TabsTrigger value="otp" className="flex-1"> <div className="relative">
کد یکبار مصرف <Input
</TabsTrigger> id="password"
</TabsList> type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="•••••••••••"
autoComplete="current-password"
dir="ltr"
className="pl-9 text-left font-mono text-[13px]"
/>
<Lock className="absolute left-3 top-2.5 size-4 text-muted-foreground" />
</div>
</div>
{errorMsg && ( <Button type="submit" disabled={loading} className="w-full text-[13.5px] font-semibold">
<Alert variant="destructive"> {loading ? (
<AlertCircle /> <>
<AlertDescription>{errorMsg}</AlertDescription> <Loader2 className="size-4 animate-spin ms-2" />
</Alert> در حال احراز هویت...
)} </>
<TabsContent value="password">
<form onSubmit={handlePasswordLogin} className="flex flex-col gap-3.5">
<div className="flex flex-col gap-1.5">
<Label htmlFor="username">نام کاربری</Label>
<Input
id="username"
required
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="president"
autoComplete="username"
/>
</div>
<div className="flex flex-col gap-1.5">
<div className="flex items-center justify-between">
<Label htmlFor="password">رمز عبور</Label>
<button
type="button"
onClick={() => setShowForgot(true)}
className="text-[11.5px] text-primary hover:underline"
>
فراموشی رمز عبور؟
</button>
</div>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
autoComplete="current-password"
/>
</div>
<Button type="submit" disabled={loading} className="mt-1 w-full">
{loading && <Loader2 className="animate-spin" />}
ورود
</Button>
</form>
</TabsContent>
<TabsContent value="otp">
{!otpSent ? (
<form onSubmit={handleSendOtp} className="flex flex-col gap-3.5">
<p className="text-[12px] leading-relaxed text-muted-foreground">
کد پنجرقمی به شماره ثبتشده شما پیامک میشود.
</p>
<div className="flex flex-col gap-1.5">
<Label htmlFor="mobile">شماره تلفن همراه</Label>
<Input
id="mobile"
type="tel"
required
dir="ltr"
value={mobileNumber}
onChange={(e) => setMobileNumber(e.target.value)}
placeholder="09130000000"
className="text-left"
/>
</div>
<Button type="submit" disabled={loading} className="mt-1 w-full">
{loading && <Loader2 className="animate-spin" />}
ارسال کد
</Button>
</form>
) : ( ) : (
<form onSubmit={handleVerifyOtp} className="flex flex-col gap-3.5"> <>
<Alert> <KeyRound className="size-4 ms-2" />
<AlertDescription className="tnum"> ورود به سامانه
کد آزمایشی:{' '} </>
<b className="font-semibold text-foreground">{simulatedCode}</b>
</AlertDescription>
</Alert>
<div className="flex flex-col gap-1.5">
<Label htmlFor="otp">کد تأیید</Label>
<Input
id="otp"
required
maxLength={5}
autoFocus
dir="ltr"
value={otpCode}
onChange={(e) => setOtpCode(e.target.value)}
className="text-center tracking-[0.4em]"
/>
</div>
<Button type="submit" disabled={loading} className="w-full">
{loading && <Loader2 className="animate-spin" />}
تأیید و ورود
</Button>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => setOtpSent(false)}
>
ویرایش شماره
</Button>
</form>
)} )}
</TabsContent> </Button>
</form>
</Tabs> <div className="rounded-lg border bg-muted/40 p-2.5 text-center text-[11.5px] text-muted-foreground">
<span>کاربر: </span>
<code className="font-mono font-semibold text-foreground">mtdemne</code>
<span className="mx-2 text-border">|</span>
<span>رمز: </span>
<code className="font-mono font-semibold text-foreground">09128121946</code>
</div>
</CardContent> </CardContent>
</Card> </Card>
<p className="mt-5 text-center text-[11px] text-muted-foreground"> <p className="mt-6 text-center text-[11px] text-muted-foreground">
مرکز رصد فناوری و آیندهپژوهی دانشگاه اصفهان مرکز رصد فناوری و آیندهپژوهی دانشگاه اصفهان
</p> </p>
</div> </div>
<ForgotPasswordModal isOpen={showForgot} onClose={() => setShowForgot(false)} />
</div> </div>
); );
}; };

View File

@ -1,8 +1,20 @@
import { UserSession, PersonaType } from '../types/futures'; import { UserSession, PersonaType } from '../types/futures';
const AUTH_STORAGE_KEY = 'ufr_auth_session_v2'; const AUTH_STORAGE_KEY = 'ufr_auth_session_v3';
export const PRESET_USERS: Record<PersonaType, UserSession> = { export const USER_MTDEMNE: UserSession = {
id: 'usr-mtdemne',
username: 'mtdemne',
fullName: 'دکتر دمنه',
role: 'admin',
roleTitleFa: 'مدیر ارشد سامانه رادار هوشمندی',
department: 'مرکز رصد و آینده‌پژوهی دانشگاه اصفهان',
token: 'jwt-demne-09128121946',
avatarUrl: ''
};
export const PRESET_USERS: Record<PersonaType | 'mtdemne', UserSession> = {
mtdemne: USER_MTDEMNE,
president: { president: {
id: 'usr-1', id: 'usr-1',
username: 'president', username: 'president',
@ -58,26 +70,24 @@ class AuthService {
// Clear legacy storage keys if present // Clear legacy storage keys if present
if (typeof window !== 'undefined' && window.localStorage) { if (typeof window !== 'undefined' && window.localStorage) {
localStorage.removeItem('ufr_auth_session_v1'); localStorage.removeItem('ufr_auth_session_v1');
localStorage.removeItem('ufr_auth_session_v2');
} }
const stored = localStorage.getItem(AUTH_STORAGE_KEY); const stored = localStorage.getItem(AUTH_STORAGE_KEY);
if (stored) { if (stored) {
const parsed = JSON.parse(stored); const parsed = JSON.parse(stored);
if (parsed?.fullName && (parsed.fullName.includes('هرسیج') || parsed.fullName.includes('حسن') || parsed.fullName.includes('حسین'))) { if (parsed?.fullName && (parsed.fullName.includes('هرسیج') || parsed.fullName.includes('حسن') || parsed.fullName.includes('حسین'))) {
const role = (parsed.role as PersonaType) || 'president'; this.currentSession = null;
this.currentSession = PRESET_USERS[role] || PRESET_USERS.president;
this.saveSession(); this.saveSession();
} else { } else {
this.currentSession = parsed; this.currentSession = parsed;
} }
} else { } else {
// Default authenticated persona for immediate institutional exploration this.currentSession = null;
this.currentSession = PRESET_USERS.president;
this.saveSession();
} }
} catch (e) { } catch (e) {
console.warn(e); console.warn(e);
this.currentSession = PRESET_USERS.president; this.currentSession = null;
} }
} }
@ -114,13 +124,20 @@ class AuthService {
return !!this.currentSession; return !!this.currentSession;
} }
public loginWithCredentials(username: string, _password: string): Promise<UserSession> { public loginWithCredentials(username: string, password?: string): Promise<UserSession> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => { setTimeout(() => {
const u = username.toLowerCase().trim(); const u = username.toLowerCase().trim();
const p = (password || '').trim();
let matched: UserSession | undefined; let matched: UserSession | undefined;
if (u === 'president' || u === 'p') { if (u === 'mtdemne' || u === 'demne') {
if (p && p !== '09128121946') {
reject(new Error('رمز عبور وارد شده نادرست است'));
return;
}
matched = USER_MTDEMNE;
} else if (u === 'president' || u === 'p') {
matched = PRESET_USERS.president; matched = PRESET_USERS.president;
} else if (u === 'researcher' || u === 'r') { } else if (u === 'researcher' || u === 'r') {
matched = PRESET_USERS.researcher; matched = PRESET_USERS.researcher;
@ -148,7 +165,7 @@ class AuthService {
} else { } else {
reject(new Error('نام کاربری یا رمز عبور اشتباه است')); reject(new Error('نام کاربری یا رمز عبور اشتباه است'));
} }
}, 500); }, 400);
}); });
} }