forked from UKSOURCE/ipv6
Adding login admin, fix ipv6 Vienamese version, add env example
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -103,12 +103,12 @@
|
||||
"highlights": [
|
||||
{
|
||||
"eyebrow": "STANDARD BENEFIT",
|
||||
"title": "Logo on backdrop, website, social media & LED",
|
||||
"title": "Logo on backdrop, LED",
|
||||
"icon": "branding_watermark"
|
||||
},
|
||||
{
|
||||
"eyebrow": "ACCESS PACK",
|
||||
"title": "01 VIP Pass + Networking Dinner",
|
||||
"title": "02 VIP Pass",
|
||||
"icon": "confirmation_number"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -103,12 +103,12 @@
|
||||
"highlights": [
|
||||
{
|
||||
"eyebrow": "QUYỀN LỢI TIÊU CHUẨN",
|
||||
"title": "Logo tại backdrop, website, social media & LED",
|
||||
"title": "Logo nổi bật trên backdrop, màn LED",
|
||||
"icon": "branding_watermark"
|
||||
},
|
||||
{
|
||||
"eyebrow": "GÓI TIẾP CẬN",
|
||||
"title": "01 vé VIP + tham dự Networking Dinner",
|
||||
"eyebrow": "QUYỀN LỢI THAM DỰ",
|
||||
"title": "Đã bao gồm 02 vé VIP đại biểu",
|
||||
"icon": "confirmation_number"
|
||||
}
|
||||
]
|
||||
@@ -116,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"
|
||||
|
||||
+6
-33
@@ -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);
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user