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 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">
|
|
||||||
رمز عبور
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="otp" className="flex-1">
|
|
||||||
کد یکبار مصرف
|
|
||||||
</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
|
<CardContent className="space-y-4">
|
||||||
{errorMsg && (
|
{errorMsg && (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive" className="py-2.5">
|
||||||
<AlertCircle />
|
<AlertCircle className="size-4" />
|
||||||
<AlertDescription>{errorMsg}</AlertDescription>
|
<AlertDescription className="text-[12px]">{errorMsg}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<TabsContent value="password">
|
<form onSubmit={handleLogin} className="space-y-4">
|
||||||
<form onSubmit={handlePasswordLogin} className="flex flex-col gap-3.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex flex-col gap-1.5">
|
<Label htmlFor="username" className="text-[12.5px] font-medium">
|
||||||
<Label htmlFor="username">نام کاربری</Label>
|
نام کاربری
|
||||||
|
</Label>
|
||||||
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
id="username"
|
id="username"
|
||||||
required
|
required
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
placeholder="president"
|
placeholder="mtdemne"
|
||||||
autoComplete="username"
|
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>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1.5">
|
<div className="space-y-1.5">
|
||||||
<div className="flex items-center justify-between">
|
<Label htmlFor="password" className="text-[12.5px] font-medium">
|
||||||
<Label htmlFor="password">رمز عبور</Label>
|
رمز عبور
|
||||||
<button
|
</Label>
|
||||||
type="button"
|
<div className="relative">
|
||||||
onClick={() => setShowForgot(true)}
|
|
||||||
className="text-[11.5px] text-primary hover:underline"
|
|
||||||
>
|
|
||||||
فراموشی رمز عبور؟
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Input
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
placeholder="••••••••"
|
placeholder="•••••••••••"
|
||||||
autoComplete="current-password"
|
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"
|
dir="ltr"
|
||||||
value={mobileNumber}
|
className="pl-9 text-left font-mono text-[13px]"
|
||||||
onChange={(e) => setMobileNumber(e.target.value)}
|
|
||||||
placeholder="09130000000"
|
|
||||||
className="text-left"
|
|
||||||
/>
|
/>
|
||||||
|
<Lock className="absolute left-3 top-2.5 size-4 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" disabled={loading} className="mt-1 w-full">
|
</div>
|
||||||
{loading && <Loader2 className="animate-spin" />}
|
|
||||||
ارسال کد
|
<Button type="submit" disabled={loading} className="w-full text-[13.5px] font-semibold">
|
||||||
</Button>
|
{loading ? (
|
||||||
</form>
|
<>
|
||||||
|
<Loader2 className="size-4 animate-spin ms-2" />
|
||||||
|
در حال احراز هویت...
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<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>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue