chore: remediate CI blockers for production readiness

This commit is contained in:
Ho Ngoc Hai
2026-05-07 13:08:20 +07:00
parent f82806e06d
commit b35ec55126
32 changed files with 401 additions and 113 deletions

View File

@@ -16,21 +16,28 @@ import { Link } from '@/i18n/navigation';
import { useAuthStore } from '@/lib/auth-store';
import { loginSchema, type LoginFormData } from '@/lib/validations/auth';
const DEMO_PASSWORD = 'Velik@2026';
const ENABLE_DEMO_ACCOUNTS = process.env['NEXT_PUBLIC_ENABLE_DEMO_ACCOUNTS'] === 'true';
const DEMO_PASSWORD = process.env['NEXT_PUBLIC_DEMO_PASSWORD'] ?? '';
const DEMO_ACCOUNTS: {
type DemoAccount = {
phone: string;
name: string;
role: 'ADMIN' | 'AGENT' | 'SELLER' | 'BUYER' | 'DEVELOPER' | 'PARK_OPERATOR';
badgeClass: string;
}[] = [
{ phone: '+84876677771', name: 'Hồ Ngọc Hải', role: 'ADMIN', badgeClass: 'bg-red-500/10 text-red-600 border-red-500/20' },
{ phone: '+84900000002', name: 'Nguyễn Văn An', role: 'AGENT', badgeClass: 'bg-blue-500/10 text-blue-600 border-blue-500/20' },
{ phone: '+84900000005', name: 'Phạm Đức Dũng', role: 'SELLER', badgeClass: 'bg-amber-500/10 text-amber-600 border-amber-500/20' },
{ phone: '+84900000004', name: 'Lê Minh Cường', role: 'BUYER', badgeClass: 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20' },
{ phone: '+84912000001', name: 'CĐT Vingroup', role: 'DEVELOPER', badgeClass: 'bg-violet-500/10 text-violet-600 border-violet-500/20' },
{ phone: '+84912000002', name: 'KCN VSIP', role: 'PARK_OPERATOR', badgeClass: 'bg-cyan-500/10 text-cyan-600 border-cyan-500/20' },
];
};
function parseDemoAccounts(): DemoAccount[] {
const raw = process.env['NEXT_PUBLIC_DEMO_ACCOUNTS'];
if (!raw) return [];
try {
const parsed = JSON.parse(raw) as DemoAccount[];
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
const DEMO_ACCOUNTS = parseDemoAccounts();
export default function LoginPage() {
const router = useRouter();
@@ -39,6 +46,7 @@ export default function LoginPage() {
const [showPassword, setShowPassword] = useState(false);
const [demoOpen, setDemoOpen] = useState(true);
const t = useTranslations('auth');
const showDemoAccounts = ENABLE_DEMO_ACCOUNTS && DEMO_PASSWORD && DEMO_ACCOUNTS.length > 0;
const oauthError = searchParams.get('error');
const oauthErrorMessage = oauthError
@@ -76,48 +84,49 @@ export default function LoginPage() {
<CardDescription>{t('loginDescription')}</CardDescription>
</CardHeader>
<CardContent>
{/* Demo accounts panel — MVP only */}
<div className="mb-4 rounded-lg border border-primary/20 bg-primary/5">
<button
type="button"
onClick={() => setDemoOpen(!demoOpen)}
className="flex w-full items-center justify-between px-3 py-2 text-sm font-medium"
aria-expanded={demoOpen}
>
<span className="inline-flex items-center gap-2">
<Sparkles className="h-4 w-4 text-primary" aria-hidden="true" />
{t('demoAccountsTitle')}
</span>
<ChevronDown
className={`h-4 w-4 text-muted-foreground transition-transform ${demoOpen ? 'rotate-180' : ''}`}
aria-hidden="true"
/>
</button>
{demoOpen && (
<div className="space-y-2 border-t border-primary/20 p-3">
<p className="text-xs text-muted-foreground">
{t('demoAccountsHint')} <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">{DEMO_PASSWORD}</code>
</p>
<ul className="flex flex-col gap-1.5">
{DEMO_ACCOUNTS.map((acc) => (
<li key={acc.phone}>
<button
type="button"
onClick={() => fillDemoAccount(acc.phone)}
className="flex w-full items-center gap-2 rounded-md border bg-card px-2.5 py-1.5 text-left text-xs transition-colors hover:border-primary/40 hover:bg-primary/5"
>
<Badge variant="outline" className={`shrink-0 ${acc.badgeClass}`}>
{acc.role}
</Badge>
<span className="flex-1 truncate font-medium">{acc.name}</span>
<span className="font-mono text-muted-foreground">{acc.phone}</span>
</button>
</li>
))}
</ul>
</div>
)}
</div>
{showDemoAccounts && (
<div className="mb-4 rounded-lg border border-primary/20 bg-primary/5">
<button
type="button"
onClick={() => setDemoOpen(!demoOpen)}
className="flex w-full items-center justify-between px-3 py-2 text-sm font-medium"
aria-expanded={demoOpen}
>
<span className="inline-flex items-center gap-2">
<Sparkles className="h-4 w-4 text-primary" aria-hidden="true" />
{t('demoAccountsTitle')}
</span>
<ChevronDown
className={`h-4 w-4 text-muted-foreground transition-transform ${demoOpen ? 'rotate-180' : ''}`}
aria-hidden="true"
/>
</button>
{demoOpen && (
<div className="space-y-2 border-t border-primary/20 p-3">
<p className="text-xs text-muted-foreground">
{t('demoAccountsHint')} <code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">{DEMO_PASSWORD}</code>
</p>
<ul className="flex flex-col gap-1.5">
{DEMO_ACCOUNTS.map((acc) => (
<li key={acc.phone}>
<button
type="button"
onClick={() => fillDemoAccount(acc.phone)}
className="flex w-full items-center gap-2 rounded-md border bg-card px-2.5 py-1.5 text-left text-xs transition-colors hover:border-primary/40 hover:bg-primary/5"
>
<Badge variant="outline" className={`shrink-0 ${acc.badgeClass}`}>
{acc.role}
</Badge>
<span className="flex-1 truncate font-medium">{acc.name}</span>
<span className="font-mono text-muted-foreground">{acc.phone}</span>
</button>
</li>
))}
</ul>
</div>
)}
</div>
)}
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4" noValidate>
{oauthErrorMessage && (