feat: simple login page with mtdemne / 09128121946 credentials and reset auto-session
This commit is contained in:
parent
719f18f553
commit
23dfc84a84
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue