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 { authService } from '../services/authService';
import { ForgotPasswordModal } from '../components/modals/ForgotPasswordModal';
import { toPersianDigits } from '../utils/persianNumbers';
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 { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
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 {
onLoginSuccess: () => void;
}
export const Login: React.FC<LoginProps> = ({ onLoginSuccess }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [mobileNumber, setMobileNumber] = useState('');
const [otpCode, setOtpCode] = useState('');
const [otpSent, setOtpSent] = useState(false);
const [simulatedCode, setSimulatedCode] = useState('');
const [username, setUsername] = useState('mtdemne');
const [password, setPassword] = useState('09128121946');
const [loading, setLoading] = useState(false);
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);
setErrorMsg('');
try {
await fn();
await authService.loginWithCredentials(username, password);
onLoginSuccess();
} catch (err) {
setErrorMsg(err instanceof Error ? err.message : fallbackError);
setErrorMsg(err instanceof Error ? err.message : 'نام کاربری یا رمز عبور اشتباه است');
} finally {
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 (
<div className="flex min-h-svh w-full items-center justify-center bg-background p-4">
<div className="w-full max-w-sm">
{/* Brand */}
{/* Brand Header */}
<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} />
</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">
افق {toPersianDigits(HORIZON_START)} تا {toPersianDigits(HORIZON_END)}
سامانه هوشمندی راهبردی افق {toPersianDigits(HORIZON_START)} تا {toPersianDigits(HORIZON_END)}
</p>
</div>
<Card data-card>
<CardContent className="p-5">
<Tabs
defaultValue="password"
onValueChange={() => setErrorMsg('')}
className="gap-4"
>
<TabsList className="w-full">
<TabsTrigger value="password" className="flex-1">
{/* Login Card */}
<Card data-card className="border shadow-md">
<CardHeader className="pb-4">
<CardTitle className="text-[16px] font-bold">ورود به سامانه</CardTitle>
<CardDescription className="text-[12px]">
جهت دسترسی به داشبورد و رادار هوشمندی، مشخصات کاربری خود را وارد فرمایید.
</CardDescription>
</CardHeader>
<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>
<TabsTrigger value="otp" className="flex-1">
کد یکبار مصرف
</TabsTrigger>
</TabsList>
</Label>
<div className="relative">
<Input
id="password"
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 && (
<Alert variant="destructive">
<AlertCircle />
<AlertDescription>{errorMsg}</AlertDescription>
</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>
<Button type="submit" disabled={loading} className="w-full text-[13.5px] font-semibold">
{loading ? (
<>
<Loader2 className="size-4 animate-spin ms-2" />
در حال احراز هویت...
</>
) : (
<form onSubmit={handleVerifyOtp} className="flex flex-col gap-3.5">
<Alert>
<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>
<>
<KeyRound className="size-4 ms-2" />
ورود به سامانه
</>
)}
</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>
</Card>
<p className="mt-5 text-center text-[11px] text-muted-foreground">
<p className="mt-6 text-center text-[11px] text-muted-foreground">
مرکز رصد فناوری و آیندهپژوهی دانشگاه اصفهان
</p>
</div>
<ForgotPasswordModal isOpen={showForgot} onClose={() => setShowForgot(false)} />
</div>
);
};

View File

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