Merge pull request 'feat/duong-11052026-IPV6SUBMIT' (#9) from feat/duong-11052026-IPV6SUBMIT into develop
Reviewed-on: UKSOURCE/ipv6#9
@@ -1 +1,3 @@
|
||||
PORT=3000
|
||||
# MongoDB — summit request queue (registration + feedback)
|
||||
MONGODB_URI=mongodb://localhost:27017/ipv6_summit
|
||||
@@ -0,0 +1,5 @@
|
||||
import LoginPageClient from "@/app/components/forms/LoginPageClient";
|
||||
|
||||
export default function LoginPage() {
|
||||
return <LoginPageClient />;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import dbConnect from "@/lib/mongodb";
|
||||
import User from "@/models/User";
|
||||
import crypto from "crypto";
|
||||
|
||||
function hashPassword(password: string) {
|
||||
return crypto.createHash("sha256").update(password).digest("hex");
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
await dbConnect();
|
||||
const { email, password } = await request.json();
|
||||
|
||||
// Tìm user và chuẩn hóa email về chữ thường
|
||||
const user = await User.findOne({ email: email.toLowerCase().trim() });
|
||||
|
||||
if (!user) {
|
||||
console.log(`Login failed: User not found -> ${email}`);
|
||||
return NextResponse.json({ error: "Invalid email or password" }, { status: 401 });
|
||||
}
|
||||
|
||||
const inputHash = hashPassword(password);
|
||||
|
||||
// Kiểm tra mật khẩu (hỗ trợ cả mật khẩu chưa hash nếu lỡ có dữ liệu cũ - nhưng ưu tiên hash)
|
||||
const isMatch = user.password === inputHash || user.password === password;
|
||||
|
||||
if (!isMatch) {
|
||||
console.log(`Login failed: Password mismatch for -> ${email}`);
|
||||
return NextResponse.json({ error: "Invalid email or password" }, { status: 401 });
|
||||
}
|
||||
|
||||
const response = NextResponse.json({ success: true });
|
||||
|
||||
response.cookies.set("admin_session", "true", {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 60 * 60 * 24,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function POST() {
|
||||
const response = NextResponse.json({ success: true });
|
||||
|
||||
// Xóa cookie bằng cách set maxAge = 0
|
||||
response.cookies.set("admin_session", "", {
|
||||
path: "/",
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -227,6 +227,16 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
const [detail, setDetail] = useState<AdminRequest | null>(null);
|
||||
const { lang, setLang } = useLanguage();
|
||||
const { admin: t } = useTranslation();
|
||||
const [userDropdown, setUserDropdown] = useState(false);
|
||||
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await fetch("/ipv6/api/auth/logout", { method: "POST" });
|
||||
router.push("/login");
|
||||
} catch (error) {
|
||||
console.error("Logout failed", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Merge real requests from server prop with translated UI labels
|
||||
const tData = {
|
||||
@@ -295,7 +305,9 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
{ href: "/admin/rejected", icon: "cancel", label: lang === "vi" ? "Từ chối" : "Rejected" },
|
||||
] as const
|
||||
).map((item) => {
|
||||
const active = pathname === item.href;
|
||||
const cleanPathname = pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
||||
const cleanHref = item.href.endsWith("/") ? item.href.slice(0, -1) : item.href;
|
||||
const active = cleanPathname === cleanHref;
|
||||
return (
|
||||
<Link
|
||||
key={item.href}
|
||||
@@ -317,13 +329,7 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
<button
|
||||
type="button"
|
||||
className="group flex flex-col items-center gap-1 text-on-surface-variant hover:text-primary mt-auto"
|
||||
>
|
||||
<span className="material-symbols-outlined text-2xl transition-all duration-300 group-hover:rotate-45">settings</span>
|
||||
<span className="font-[var(--font-label-caps)] text-[8px] uppercase tracking-widest">{lang === "vi" ? "Cài đặt" : "Settings"}</span>
|
||||
</button>
|
||||
{/* Settings button removed as per request */}
|
||||
</aside>
|
||||
|
||||
<div className="flex-1 ml-20 md:ml-24 flex flex-col min-w-0">
|
||||
@@ -382,10 +388,33 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData;
|
||||
<span>VIE</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="font-[var(--font-label-caps)] text-[10px] text-primary uppercase tracking-widest">
|
||||
{t.meta.user.role}
|
||||
</span>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setUserDropdown(!userDropdown)}
|
||||
className="flex flex-col items-end group"
|
||||
>
|
||||
<span className="font-[var(--font-label-caps)] text-[10px] text-primary uppercase tracking-widest group-hover:text-primary/80 transition-colors">
|
||||
{t.meta.user.role}
|
||||
</span>
|
||||
<span className="material-symbols-outlined text-outline group-hover:text-primary transition-colors text-xs">
|
||||
{userDropdown ? "expand_less" : "expand_more"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{userDropdown && (
|
||||
<div className="absolute right-0 mt-2 w-48 glass-panel rounded-xl shadow-2xl border border-white/10 z-[60] overflow-hidden">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full px-4 py-3 flex items-center gap-3 text-left hover:bg-white/5 text-on-surface transition-colors border-b border-white/5"
|
||||
>
|
||||
<span className="material-symbols-outlined text-red-400 text-lg">logout</span>
|
||||
<span className="text-xs font-[var(--font-label-caps)] uppercase tracking-wider">
|
||||
{lang === "vi" ? "Đăng xuất" : "Log Out"}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useLanguage } from "@/app/context/LanguageContext";
|
||||
|
||||
export default function LoginPageClient() {
|
||||
const { lang } = useLanguage();
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const backLabel = lang === "vi" ? "Quay Về Trang Chủ" : "Back to Home";
|
||||
const titleLabel = lang === "vi" ? "Đăng Nhập Quản Trị" : "Admin Login";
|
||||
const emailLabel = lang === "vi" ? "Email" : "Email Address";
|
||||
const passwordLabel = lang === "vi" ? "Mật Khẩu" : "Password";
|
||||
const buttonLabel = lang === "vi" ? "Đăng Nhập" : "Log In";
|
||||
const loadingLabel = lang === "vi" ? "Đang xử lý..." : "Processing...";
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError("");
|
||||
|
||||
try {
|
||||
const res = await fetch("/ipv6/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
router.push("/admin");
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || (lang === "vi" ? "Đăng nhập thất bại" : "Login failed"));
|
||||
}
|
||||
} catch (err) {
|
||||
setError(lang === "vi" ? "Đã xảy ra lỗi" : "An error occurred");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="pt-[120px] pb-[80px] px-[var(--spacing-gutter)] flex justify-center min-h-screen">
|
||||
<div className="w-full max-w-[1280px]">
|
||||
<div className="max-w-[500px] mx-auto">
|
||||
<div className="mb-[32px]">
|
||||
<Link
|
||||
className="group flex items-center gap-2 text-outline hover:text-primary transition-colors"
|
||||
href="/"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[20px] transition-transform group-hover:-translate-x-1">
|
||||
arrow_back
|
||||
</span>
|
||||
{backLabel}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="mb-[32px]">
|
||||
<h1 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3vw+1rem,3.25rem)] text-on-surface mb-2 leading-[1.1]">
|
||||
{titleLabel}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="glass-panel p-[32px] md:p-12 rounded-xl shadow-2xl relative overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-[1px] bg-gradient-to-r from-transparent via-primary/20 to-transparent" />
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-error/10 border border-error/20 text-error px-4 py-3 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider text-xs">{emailLabel}</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all focus:border-primary/50 outline-none"
|
||||
placeholder=""
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-outline uppercase tracking-wider text-xs">{passwordLabel}</label>
|
||||
<input
|
||||
type="password"
|
||||
required
|
||||
className="w-full bg-surface-container-low border border-white/10 rounded-lg px-4 py-3 text-on-surface placeholder:text-outline/40 transition-all focus:border-primary/50 outline-none"
|
||||
placeholder=""
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="gold-gradient-bg text-on-primary w-full px-6 py-4 rounded-xl font-[var(--font-display-lg)] text-lg uppercase tracking-wider hover:shadow-[0_0_30px_rgba(197,160,89,0.4)] transition-all duration-300 active:scale-[0.98] disabled:opacity-60"
|
||||
>
|
||||
{loading ? loadingLabel : buttonLabel}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import HeroSection from "@/app/components/home/HeroSection";
|
||||
import VisionSection from "@/app/components/home/VisionSection";
|
||||
import StatsSection from "@/app/components/home/StatsSection";
|
||||
import AgendaSection from "@/app/components/home/AgendaSection";
|
||||
import PartnersCarousel from "@/app/components/home/PartnersCarousel";
|
||||
|
||||
export default function HomePageClient() {
|
||||
const { home } = useTranslation();
|
||||
@@ -15,6 +16,7 @@ export default function HomePageClient() {
|
||||
<VisionSection data={home.vision} />
|
||||
<StatsSection data={home.stats} />
|
||||
<AgendaSection data={home.agenda} />
|
||||
<PartnersCarousel />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
// Tên file chính xác trong public/assets/img/carousel/
|
||||
const ROW_1 = [
|
||||
{ name: "NVIDIA", file: "Nvidia_logo.svg.png" },
|
||||
{ name: "Maxis", file: "Maxis-logo.png" },
|
||||
{ name: "Singtel", file: "Singtel_logo.svg.png" },
|
||||
{ name: "NIDA", file: "nida.png" },
|
||||
{ name: "VNNIC", file: "vnnic.png" },
|
||||
{ name: "Netflix", file: "Logonetflix.png" },
|
||||
{ name: "Microsoft",file: "Microsoft-Logo.png" },
|
||||
{ name: "Alibaba", file: "Alibaba-Logo.png" },
|
||||
];
|
||||
|
||||
const ROW_2 = [
|
||||
{ name: "Tencent", file: "tencent.png" },
|
||||
{ name: "AirTrunk", file: "airtrunk.png" },
|
||||
{ name: "Celcomdigi",file: "celcomdigi.png" },
|
||||
{ name: "Grab", file: "grab.png" },
|
||||
{ name: "Logo 1", file: "logo_transparent.png" },
|
||||
{ name: "Logo 2", file: "Equinix_logo.svg.png" },
|
||||
{ name: "Logo 3", file: "XL_Axiata-Logo.wine.png" },
|
||||
{ name: "Logo 4", file: "Huawei_Standard_logo.svg.png" },
|
||||
];
|
||||
|
||||
function CarouselRow({
|
||||
items,
|
||||
base,
|
||||
reverse = false,
|
||||
}: {
|
||||
items: { name: string; file: string }[];
|
||||
base: string;
|
||||
reverse?: boolean;
|
||||
}) {
|
||||
// Lặp 3 lần để đảm bảo không bao giờ thấy khoảng trống
|
||||
const tripled = [...items, ...items, ...items];
|
||||
// Mỗi item: w=150px, gap=20px → 1 set = items.length * 170 - 20
|
||||
const setWidth = items.length * 150 + (items.length - 1) * 20;
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden w-full">
|
||||
<div
|
||||
className="flex gap-5 w-max"
|
||||
style={{
|
||||
animation: `${reverse ? "scroll-reverse" : "scroll-fwd"} ${items.length * 3}s linear infinite`,
|
||||
["--set-width" as string]: `${setWidth}px`,
|
||||
}}
|
||||
>
|
||||
{tripled.map((item, i) => (
|
||||
<div
|
||||
key={`${item.file}-${i}`}
|
||||
className="flex items-center justify-center shrink-0 w-[150px] h-[76px] rounded-xl border border-white/10 hover:border-primary/40 transition-colors duration-300 overflow-hidden"
|
||||
style={{ background: "rgba(255,255,255,0.82)" }}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={`${base}/assets/img/carousel/${item.file}`}
|
||||
alt={item.name}
|
||||
style={{
|
||||
maxHeight: 48,
|
||||
maxWidth: 118,
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
objectFit: "contain",
|
||||
mixBlendMode: "multiply",
|
||||
filter: "contrast(1.05)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function PartnersCarousel() {
|
||||
const [base, setBase] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const match = window.location.pathname.match(/^(\/ipv6)/);
|
||||
setBase(match ? match[1] : "");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="w-full py-16 lg:py-24 overflow-hidden border-t border-white/5">
|
||||
<div className="mb-10 px-[var(--spacing-gutter)] max-w-hd mx-auto">
|
||||
<p className="text-primary font-[var(--font-label-caps)] text-xs tracking-[0.3em] uppercase mb-3">
|
||||
Partners & Sponsors
|
||||
</p>
|
||||
<h2 className="text-on-surface font-[var(--font-display-lg)] text-2xl lg:text-4xl font-bold">
|
||||
Trusted by Industry Leaders
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-5">
|
||||
<CarouselRow items={ROW_1} base={base} reverse={false} />
|
||||
<CarouselRow items={ROW_2} base={base} reverse={true} />
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -29,7 +29,9 @@ export default function HeaderClient({
|
||||
};
|
||||
|
||||
const registerLabel = lang === "vi" ? "Đăng Ký" : "Register";
|
||||
const loginLabel = lang === "vi" ? "Đăng Nhập" : "Log In";
|
||||
const registerNowLabel = lang === "vi" ? "Đăng Ký Ngay" : "Register Now";
|
||||
const loginNowLabel = lang === "vi" ? "Đăng Nhập" : "Log In";
|
||||
const backLabel = lang === "vi" ? "Trang Chủ" : "Home";
|
||||
|
||||
useEffect(() => {
|
||||
@@ -106,6 +108,12 @@ export default function HeaderClient({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/login"
|
||||
className="hidden sm:inline-flex gold-gradient-bg text-on-primary px-4 lg:px-6 py-2 lg:py-2.5 font-[var(--font-body-base)] text-xs lg:text-sm font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10"
|
||||
>
|
||||
{loginLabel}
|
||||
</Link>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="hidden sm:inline-flex gold-gradient-bg text-on-primary px-4 lg:px-6 py-2 lg:py-2.5 font-[var(--font-body-base)] text-xs lg:text-sm font-bold uppercase tracking-wider rounded-lg active:scale-95 transition-transform shadow-lg shadow-primary/10"
|
||||
@@ -205,6 +213,13 @@ export default function HeaderClient({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
href="/login"
|
||||
className="gold-gradient-bg text-on-primary w-full py-5 font-[var(--font-body-base)] text-lg font-bold uppercase tracking-widest rounded-xl text-center shadow-2xl"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
>
|
||||
{loginNowLabel}
|
||||
</Link>
|
||||
<Link
|
||||
href="/registration"
|
||||
className="gold-gradient-bg text-on-primary w-full py-5 font-[var(--font-body-base)] text-lg font-bold uppercase tracking-widest rounded-xl text-center shadow-2xl"
|
||||
|
||||
@@ -19,28 +19,28 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) {
|
||||
|
||||
<div className="relative z-10 grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-24 items-center">
|
||||
{/* Left Content */}
|
||||
<div className="space-y-12">
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className="font-[var(--font-display-lg)] text-[clamp(2rem,4vw,4rem)] text-on-surface mb-6 lg:mb-8 leading-[1.1]">
|
||||
<h2 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3.5vw,3rem)] text-on-surface mb-4 leading-[1.1]">
|
||||
{data.title}
|
||||
</h2>
|
||||
<p className="font-body-base text-base lg:text-[20px] text-on-surface-variant max-w-xl leading-relaxed opacity-80">
|
||||
<p className="font-body-base text-sm lg:text-[17px] text-on-surface-variant max-w-xl leading-relaxed opacity-80">
|
||||
{data.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-8 lg:gap-12">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||
{/* Phone */}
|
||||
<div className="group">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-16 h-16 border border-primary/20 bg-primary/5 rounded-2xl flex items-center justify-center group-hover:border-primary/50 group-hover:bg-primary/10 transition-all duration-300">
|
||||
<span className="material-symbols-outlined text-primary text-3xl">call</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-13 h-13 shrink-0 border border-primary/20 bg-primary/5 rounded-xl flex items-center justify-center group-hover:border-primary/50 group-hover:bg-primary/10 transition-all duration-300">
|
||||
<span className="material-symbols-outlined text-primary text-2xl">call</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-[var(--font-label-caps)] text-[10px] lg:text-xs font-bold tracking-[0.25em] uppercase text-primary mb-2 opacity-60">
|
||||
<div className="min-w-0">
|
||||
<p className="font-[var(--font-label-caps)] text-[10px] font-bold tracking-[0.2em] uppercase text-primary mb-1 opacity-60">
|
||||
{data.phone.label}
|
||||
</p>
|
||||
<p className="font-[var(--font-display-lg)] text-lg lg:text-2xl text-on-surface">
|
||||
<p className="font-[var(--font-display-lg)] text-base text-on-surface whitespace-nowrap">
|
||||
{data.phone.value}
|
||||
</p>
|
||||
</div>
|
||||
@@ -49,15 +49,15 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) {
|
||||
|
||||
{/* Email */}
|
||||
<div className="group">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="w-16 h-16 border border-primary/20 bg-primary/5 rounded-2xl flex items-center justify-center group-hover:border-primary/50 group-hover:bg-primary/10 transition-all duration-300">
|
||||
<span className="material-symbols-outlined text-primary text-3xl">mail</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-13 h-13 shrink-0 border border-primary/20 bg-primary/5 rounded-xl flex items-center justify-center group-hover:border-primary/50 group-hover:bg-primary/10 transition-all duration-300">
|
||||
<span className="material-symbols-outlined text-primary text-2xl">mail</span>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-[var(--font-label-caps)] text-[10px] lg:text-xs font-bold tracking-[0.25em] uppercase text-primary mb-2 opacity-60">
|
||||
<div className="min-w-0">
|
||||
<p className="font-[var(--font-label-caps)] text-[10px] font-bold tracking-[0.2em] uppercase text-primary mb-1 opacity-60">
|
||||
{data.email.label}
|
||||
</p>
|
||||
<p className="font-[var(--font-display-lg)] text-lg lg:text-2xl text-on-surface break-all">
|
||||
<p className="font-[var(--font-display-lg)] text-base text-on-surface truncate">
|
||||
{data.email.value}
|
||||
</p>
|
||||
</div>
|
||||
@@ -67,7 +67,7 @@ export default function SponsorContact({ data }: { data: SponsorContactData }) {
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="gold-gradient-bg text-on-primary w-full sm:w-auto px-12 lg:px-16 py-5 lg:py-6 rounded-2xl font-bold uppercase tracking-widest text-sm lg:text-base shadow-2xl shadow-primary/20 hover:shadow-primary/40 transition-all duration-500 active:scale-95 flex items-center justify-center gap-4 group"
|
||||
className="gold-gradient-bg text-on-primary w-full sm:w-auto px-8 lg:px-10 py-3.5 rounded-xl font-bold uppercase tracking-widest text-sm shadow-2xl shadow-primary/20 hover:shadow-primary/40 transition-all duration-500 active:scale-95 flex items-center justify-center gap-3 group"
|
||||
>
|
||||
{data.cta}
|
||||
<span className="material-symbols-outlined group-hover:translate-x-2 transition-transform">
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useLanguage } from "@/app/context/LanguageContext";
|
||||
|
||||
type SponsorHeroData = {
|
||||
eyebrow: string;
|
||||
@@ -9,7 +12,15 @@ type SponsorHeroData = {
|
||||
secondaryCta: { label: string; href: string };
|
||||
};
|
||||
|
||||
const PROSPECTUS_LINKS: Record<string, string> = {
|
||||
en: "https://drive.google.com/file/d/12uNVGFQ5ixuL2UwJpb6d-x-iuYjAt7Pc/view",
|
||||
vi: "https://drive.google.com/file/d/1YDpYwdWJHbxKa66u0Iq5PQ4aEaWgvkfI/view",
|
||||
};
|
||||
|
||||
export default function SponsorHero({ data }: { data: SponsorHeroData }) {
|
||||
const { lang } = useLanguage();
|
||||
const prospectusHref = PROSPECTUS_LINKS[lang] ?? PROSPECTUS_LINKS.en;
|
||||
|
||||
return (
|
||||
<section className="relative py-16 md:py-32 overflow-hidden text-center px-[var(--spacing-gutter)]">
|
||||
<div className="relative z-10 max-w-4xl mx-auto w-full">
|
||||
@@ -36,7 +47,9 @@ export default function SponsorHero({ data }: { data: SponsorHeroData }) {
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 md:gap-6">
|
||||
<Link
|
||||
href={data.primaryCta.href}
|
||||
href={prospectusHref}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="gold-gradient-bg text-on-primary px-10 py-4 rounded-xl font-bold uppercase tracking-widest text-xs md:text-sm shadow-xl shadow-primary/20 hover:shadow-primary/30 transition-all duration-300 active:scale-95 w-full sm:w-auto"
|
||||
>
|
||||
{data.primaryCta.label}
|
||||
|
||||
@@ -2,6 +2,7 @@ type ItemCard = {
|
||||
icon: string;
|
||||
title: string;
|
||||
status: string;
|
||||
benefits?: string[];
|
||||
};
|
||||
|
||||
type Highlight = {
|
||||
@@ -24,6 +25,11 @@ export default function SponsorItemBased({ data }: { data: SponsorItemBasedData
|
||||
<div className="max-w-hd mx-auto">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-16 lg:mb-28">
|
||||
{data.eyebrow && (
|
||||
<p className="text-primary font-[var(--font-label-caps)] text-xs tracking-[0.3em] uppercase mb-4 opacity-80">
|
||||
{data.eyebrow}
|
||||
</p>
|
||||
)}
|
||||
<h2 className="font-[var(--font-display-lg)] text-[clamp(1.75rem,3.5vw,3.5rem)] mb-4 md:mb-6 text-on-surface leading-tight">
|
||||
{data.title}
|
||||
</h2>
|
||||
@@ -33,21 +39,30 @@ export default function SponsorItemBased({ data }: { data: SponsorItemBasedData
|
||||
</div>
|
||||
|
||||
{/* 4-col cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-10 mb-16 lg:mb-24">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 lg:gap-8 mb-16 lg:mb-24">
|
||||
{data.items.map((item) => (
|
||||
<div
|
||||
key={item.title}
|
||||
className="glass-panel p-8 lg:p-12 rounded-3xl text-center group hover:bg-surface-container-high transition-all duration-500 transform hover:-translate-y-2 border-primary/10"
|
||||
className="glass-panel p-8 rounded-3xl group hover:bg-surface-container-high transition-all duration-500 transform hover:-translate-y-2 border-primary/10 flex flex-col items-center text-center"
|
||||
>
|
||||
<div className="w-16 h-16 gold-gradient-bg rounded-2xl flex items-center justify-center mx-auto mb-8 rotate-3 group-hover:rotate-0 transition-transform duration-500 shadow-xl shadow-primary/20">
|
||||
<span className="material-symbols-outlined text-on-primary text-3xl lg:text-4xl">
|
||||
<div className="w-14 h-14 gold-gradient-bg rounded-2xl flex items-center justify-center mb-6 rotate-3 group-hover:rotate-0 transition-transform duration-500 shadow-xl shadow-primary/20">
|
||||
<span className="material-symbols-outlined text-on-primary text-2xl">
|
||||
{item.icon}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="font-[var(--font-display-lg)] text-xl lg:text-2xl mb-3 text-on-surface">
|
||||
<h4 className="font-[var(--font-display-lg)] text-lg lg:text-xl mb-2 text-on-surface leading-tight">
|
||||
{item.title}
|
||||
</h4>
|
||||
<p className="text-[11px] font-bold tracking-[0.3em] uppercase text-primary">
|
||||
{item.benefits && item.benefits.length > 0 && (
|
||||
<ul className="mt-3 mb-4 space-y-1.5 flex-1">
|
||||
{item.benefits.map((b) => (
|
||||
<li key={b} className="text-sm lg:text-base text-on-surface-variant/80">
|
||||
{b}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
<p className="text-xs font-bold tracking-[0.3em] uppercase text-primary mt-auto">
|
||||
{item.status}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"title": "Phản Hồi Sự Kiện",
|
||||
"subtitle": "Ý kiến của bạn định hướng tương lai hạ tầng IPv6. Hãy giúp chúng tôi cải thiện trải nghiệm năm 2027.",
|
||||
"title": "Khảo sát Sự Kiện",
|
||||
"subtitle": "Giúp chúng tôi nâng tầm trải nghiệm 2027, thúc đẩy tương lai hạ tầng IPv6 bằng đánh giá của bạn.",
|
||||
"sections": [
|
||||
{
|
||||
"id": "overall",
|
||||
"title": "1. TRẢI NGHIỆM TỔNG THỂ",
|
||||
"title": "1. MỨC ĐỘ HÀI LÒNG TỔNG QUAN",
|
||||
"subtitle": "Tổng quan về trải nghiệm hội nghị của bạn",
|
||||
"questions": [
|
||||
{
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"hero": {
|
||||
"badgeIcon": "calendar_today",
|
||||
"badgeText": "Tháng 6/2026 | TP. Hồ Chí Minh, Việt Nam",
|
||||
"title": "Hội Nghị IPv6 cho AI & Trung Tâm Dữ Liệu 2026",
|
||||
"subtitle": "Khai Phá Tiềm Năng Hạ Tầng Thế Hệ Mới. Cùng các nhà lãnh đạo toàn cầu định hình lại kết nối số ASEAN trong kỷ nguyên AI.",
|
||||
"title": "IPv6 for AI & Data Centre Summit 2026",
|
||||
"subtitle": "Khám phá tiềm năng Hạ tầng thế hệ mới. Chung sức định hình kỷ nguyên kết nối số tại ASEAN cùng doanh nghiệp hàng đầu quốc tế.",
|
||||
"primaryCta": { "label": "Đăng Ký", "href": "/registration" },
|
||||
"secondaryCta": { "label": "Xem Lịch Trình", "href": "#agenda" }
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"title": "Đăng Ký Tham Dự Hội Nghị",
|
||||
"subtitle": "Đảm bảo sự hiện diện của bạn ở tuyến đầu đổi mới IPv6 cho AI và Trung Tâm Dữ Liệu.",
|
||||
"subtitle": "Nắm bắt cơ hội tiên phong thời kỳ đổi mới IPv6 cho AI và trung tâm dữ liệu.",
|
||||
"sections": {
|
||||
"personal": {
|
||||
"title": "Thông Tin Cá Nhân",
|
||||
@@ -9,13 +9,13 @@
|
||||
"email": "Email Doanh Nghiệp"
|
||||
},
|
||||
"professional": {
|
||||
"title": "Thông Tin Nghề Nghiệp",
|
||||
"company": "Tên Công Ty",
|
||||
"jobTitle": "Chức Danh"
|
||||
"title": "Thông Tin Doanh Nghiệp",
|
||||
"company": "Tên Doanh Nghiệp",
|
||||
"jobTitle": "Vị Trí"
|
||||
},
|
||||
"industry": {
|
||||
"title": "Ngành Nghề",
|
||||
"label": "Chọn Ngành Nghề",
|
||||
"title": "Lĩnh Vực",
|
||||
"label": "Chọn Lĩnh Vực",
|
||||
"placeholder": "Vui lòng chọn...",
|
||||
"options": [
|
||||
{ "value": "government", "label": "Chính Phủ" },
|
||||
@@ -27,8 +27,8 @@
|
||||
},
|
||||
"additional": {
|
||||
"title": "Thông Tin Bổ Sung",
|
||||
"label": "Yêu cầu đặc biệt hoặc chế độ ăn kiêng",
|
||||
"placeholder": "Tùy chọn ăn chay, nhu cầu tiếp cận, v.v."
|
||||
"label": "Yêu cầu đặc biệt hoặc chế độ dinh dưỡng",
|
||||
"placeholder": "Chế độ ăn chay, hỗ trợ người khuyết tật, v.v"
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
|
||||
@@ -71,24 +71,44 @@
|
||||
]
|
||||
},
|
||||
"itemBased": {
|
||||
"eyebrow": "À LA CARTE",
|
||||
"eyebrow": "ENGAGEMENT CATEGORIES",
|
||||
"title": "Item-Based Sponsorship",
|
||||
"description": "Targeted branding opportunities for specific summit pillars",
|
||||
"items": [
|
||||
{ "icon": "hotel", "title": "Hotel Accommodation", "status": "Limited Space" },
|
||||
{ "icon": "meeting_room", "title": "Conference Hall", "status": "Sold Out" },
|
||||
{ "icon": "groups", "title": "Networking Zone", "status": "Available" },
|
||||
{ "icon": "newsmode", "title": "Media Partner", "status": "Apply Now" }
|
||||
{
|
||||
"icon": "location_city",
|
||||
"title": "Venue Sponsor",
|
||||
"status": "Register Now",
|
||||
"benefits": ["Hotel venue", "Conference hall", "Networking area"]
|
||||
},
|
||||
{
|
||||
"icon": "computer",
|
||||
"title": "Tech Sponsor",
|
||||
"status": "Register Now",
|
||||
"benefits": ["AV equipment", "WiFi / Internet", "Tech support"]
|
||||
},
|
||||
{
|
||||
"icon": "newspaper",
|
||||
"title": "Media Sponsor",
|
||||
"status": "Register Now",
|
||||
"benefits": ["Press coverage", "Social media", "Media coverage"]
|
||||
},
|
||||
{
|
||||
"icon": "card_giftcard",
|
||||
"title": "Gift & F&B Sponsor",
|
||||
"status": "Register Now",
|
||||
"benefits": ["Gift set", "Coffee break", "Networking dinner"]
|
||||
}
|
||||
],
|
||||
"highlights": [
|
||||
{
|
||||
"eyebrow": "STANDARD BENEFIT",
|
||||
"title": "Logo on backdrop & LED screens",
|
||||
"title": "Logo on backdrop, LED",
|
||||
"icon": "branding_watermark"
|
||||
},
|
||||
{
|
||||
"eyebrow": "ACCESS PACK",
|
||||
"title": "02 VIP Delegate tickets included",
|
||||
"title": "02 VIP Pass",
|
||||
"icon": "confirmation_number"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -71,24 +71,44 @@
|
||||
]
|
||||
},
|
||||
"itemBased": {
|
||||
"eyebrow": "À LA CARTE",
|
||||
"eyebrow": "HẠNG MỤC ĐỒNG HÀNH",
|
||||
"title": "Tài Trợ Theo Hạng Mục",
|
||||
"description": "Cơ hội thương hiệu có mục tiêu cho các trụ cột cụ thể của hội nghị",
|
||||
"items": [
|
||||
{ "icon": "hotel", "title": "Chỗ Ở Khách Sạn", "status": "Còn Ít Chỗ" },
|
||||
{ "icon": "meeting_room", "title": "Hội Trường Hội Nghị", "status": "Đã Hết" },
|
||||
{ "icon": "groups", "title": "Khu Vực Kết Nối", "status": "Còn Trống" },
|
||||
{ "icon": "newsmode", "title": "Đối Tác Truyền Thông", "status": "Đăng Ký Ngay" }
|
||||
{
|
||||
"icon": "location_city",
|
||||
"title": "Tài Trợ Địa Điểm",
|
||||
"status": "Đăng Ký Ngay",
|
||||
"benefits": ["Khách sạn", "Hội trường", "Khu vực networking"]
|
||||
},
|
||||
{
|
||||
"icon": "computer",
|
||||
"title": "Tài Trợ Công Nghệ",
|
||||
"status": "Đăng Ký Ngay",
|
||||
"benefits": ["Thiết bị trình chiếu", "WiFi / Internet", "Hạ tầng kỹ thuật"]
|
||||
},
|
||||
{
|
||||
"icon": "newspaper",
|
||||
"title": "Tài Trợ Truyền Thông",
|
||||
"status": "Đăng Ký Ngay",
|
||||
"benefits": ["Báo chí", "Social media", "Media coverage"]
|
||||
},
|
||||
{
|
||||
"icon": "card_giftcard",
|
||||
"title": "Tài Trợ Quà Tặng / F&B",
|
||||
"status": "Đăng Ký Ngay",
|
||||
"benefits": ["Gift set", "Coffee break", "Networking dinner"]
|
||||
}
|
||||
],
|
||||
"highlights": [
|
||||
{
|
||||
"eyebrow": "QUYỀN LỢI TIÊU CHUẨN",
|
||||
"title": "Logo trên phông & màn hình LED",
|
||||
"title": "Logo nổi bật trên backdrop, màn LED",
|
||||
"icon": "branding_watermark"
|
||||
},
|
||||
{
|
||||
"eyebrow": "GÓI TIẾP CẬN",
|
||||
"title": "02 vé VIP đại biểu được bao gồm",
|
||||
"eyebrow": "QUYỀN LỢI THAM DỰ",
|
||||
"title": "Đã bao gồm 02 vé VIP đại biểu",
|
||||
"icon": "confirmation_number"
|
||||
}
|
||||
]
|
||||
@@ -96,16 +116,16 @@
|
||||
"contact": {
|
||||
"id": "contact",
|
||||
"title": "Hợp Tác Cùng Chúng Tôi",
|
||||
"description": "Nâng tầm thương hiệu của bạn trong hành lang công nghệ Ấn Độ - Thái Bình Dương. Hội nghị của chúng tôi mang đến khả năng tiếp cận vô song tới các nhà ra quyết định, nhà nghiên cứu và quan chức chính phủ đang thúc đẩy thế hệ hạ tầng trung tâm dữ liệu và phát triển AI.",
|
||||
"description": "Nâng tầm thương hiệu doanh nghiệp trên khu vực công nghệ Ấn Độ - Thái Bình Dương. Hội thảo mang đến cơ hội hiếm có đến các tổ chức chính phủ, lãnh đạo chủ chốt, đơn vị nghiên cứu trực tiếp tham gia vào quá trình phát triển AI và Cơ sở hạ tầng dữ liệu số.",
|
||||
"phone": {
|
||||
"label": "ĐIỆN THOẠI",
|
||||
"value": "+84 941 523 498"
|
||||
},
|
||||
"email": {
|
||||
"label": "EMAIL TRỰC TIẾP",
|
||||
"label": "EMAIL TƯ VẤN",
|
||||
"value": "events@techvanguard.vn"
|
||||
},
|
||||
"cta": "Liên Hệ Để Tài Trợ",
|
||||
"cta": "Kết nối nhà tài trợ",
|
||||
"image": {
|
||||
"src": "https://lh3.googleusercontent.com/aida-public/AB6AXuATSk4rhRYAo9_OfX0cJwIC7rUdUmzD9kO7cJM24ECjPHwglU_s0wjBn6gdK0d7tk4ExLvG9DUJQ1L-PbP5ucVH2THO05fev6bWgyvpkElf_94JXJNeWj0Mo4-5-l2ofY-kHE6kPLhitbRzarR7u41favHCt_V9WM0yYb7tcZZQM8eAN5jIVT5OVKSGvfpZNYe05dJm_h3pW0qkWhX1XrUtg_xhhjdvTi-Mei7zHDUkwwRrydg7BHmO17W6ScfALH5rMXojTHub7UDN",
|
||||
"alt": "Cuộc họp kinh doanh chuyên nghiệp trong phòng hội thảo công nghệ cao"
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: "Nasalization";
|
||||
src: url("/assets/webfonts/Nasalization Rg.otf") format("opentype");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@theme {
|
||||
/* IPV6 Summit design tokens */
|
||||
--color-primary: #c5a059;
|
||||
@@ -16,9 +24,9 @@
|
||||
--color-on-surface-variant: #e0e0e0;
|
||||
--color-outline: #c5a059;
|
||||
|
||||
--font-body-base: "Inter", system-ui, sans-serif;
|
||||
--font-display-lg: "Space Grotesk", system-ui, sans-serif;
|
||||
--font-label-caps: "Geist", system-ui, sans-serif;
|
||||
--font-body-base: "Nasalization", system-ui, sans-serif;
|
||||
--font-display-lg: "Nasalization", system-ui, sans-serif;
|
||||
--font-label-caps: "Nasalization", system-ui, sans-serif;
|
||||
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
|
||||
@@ -37,7 +45,7 @@ html {
|
||||
body {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-on-surface-variant);
|
||||
font-family: var(--font-body-base);
|
||||
font-family: "Nasalization", system-ui, sans-serif;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
@@ -98,3 +106,19 @@ textarea::placeholder {
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
|
||||
|
||||
/* Partners Carousel animations */
|
||||
@keyframes scroll-fwd {
|
||||
0% { transform: translateX(0); }
|
||||
100% { transform: translateX(calc(-1 * var(--set-width) - 20px)); }
|
||||
}
|
||||
|
||||
@keyframes scroll-reverse {
|
||||
0% { transform: translateX(calc(-1 * var(--set-width) - 20px)); }
|
||||
100% { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.animate-scroll:hover,
|
||||
.animate-scroll-reverse:hover {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
@@ -1,41 +1,14 @@
|
||||
import type { NextRequest } from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
function unauthorized() {
|
||||
return new NextResponse("Authentication required", {
|
||||
status: 401,
|
||||
headers: {
|
||||
"WWW-Authenticate": 'Basic realm="IPv6 Admin"',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function middleware(request: NextRequest) {
|
||||
const user = process.env.ADMIN_BASIC_AUTH_USER;
|
||||
const pass = process.env.ADMIN_BASIC_AUTH_PASS;
|
||||
const session = request.cookies.get("admin_session");
|
||||
|
||||
if (!user || !pass) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
const auth = request.headers.get("authorization");
|
||||
if (!auth?.startsWith("Basic ")) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = atob(auth.slice(6));
|
||||
} catch {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
const colon = decoded.indexOf(":");
|
||||
const u = colon >= 0 ? decoded.slice(0, colon) : "";
|
||||
const p = colon >= 0 ? decoded.slice(colon + 1) : "";
|
||||
|
||||
if (u !== user || p !== pass) {
|
||||
return new NextResponse("Unauthorized", { status: 401 });
|
||||
if (!session || session.value !== "true") {
|
||||
// If not authenticated and trying to access admin, redirect to login
|
||||
const url = request.nextUrl.clone();
|
||||
url.pathname = "/login";
|
||||
return NextResponse.redirect(url);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import mongoose, { Schema, Document } from "mongoose";
|
||||
|
||||
export interface IUser extends Document {
|
||||
email: string;
|
||||
password: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const UserSchema: Schema = new Schema(
|
||||
{
|
||||
email: { type: String, required: true, unique: true },
|
||||
password: { type: String, required: true },
|
||||
},
|
||||
{ timestamps: true }
|
||||
);
|
||||
|
||||
export default mongoose.models.User || mongoose.model<IUser>("User", UserSchema);
|
||||
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 212 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 132 KiB |
@@ -0,0 +1,46 @@
|
||||
import mongoose from "mongoose";
|
||||
import User from "../models/User";
|
||||
import * as dotenv from "dotenv";
|
||||
import path from "path";
|
||||
import crypto from "crypto";
|
||||
|
||||
dotenv.config({ path: path.join(__dirname, "../.env") });
|
||||
|
||||
function hashPassword(password: string) {
|
||||
return crypto.createHash("sha256").update(password).digest("hex");
|
||||
}
|
||||
|
||||
async function seedAdmin() {
|
||||
const MONGODB_URI = process.env.MONGODB_URI;
|
||||
|
||||
if (!MONGODB_URI) {
|
||||
console.error("MONGODB_URI not found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
await mongoose.connect(MONGODB_URI);
|
||||
console.log("Connected to MongoDB");
|
||||
|
||||
const adminEmail = "adminipv6@mail.com".toLowerCase().trim();
|
||||
const adminPassword = "adminipv6A@a";
|
||||
const hashedPassword = hashPassword(adminPassword);
|
||||
|
||||
// Xóa sạch user cũ để tránh xung đột
|
||||
await User.deleteMany({ email: adminEmail });
|
||||
|
||||
await User.create({
|
||||
email: adminEmail,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
console.log("SUCCESS: Admin account created/refreshed with hashed password.");
|
||||
console.log(`Email: ${adminEmail}`);
|
||||
} catch (error) {
|
||||
console.error("Error:", error);
|
||||
} finally {
|
||||
await mongoose.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
seedAdmin();
|
||||