From c7f09d941e207356d5f553aa1894fb845134620a Mon Sep 17 00:00:00 2001 From: DuongNguyendev Date: Fri, 15 May 2026 19:52:34 +0700 Subject: [PATCH] Adding login admin, fix ipv6 Vienamese version, add env example --- .env.example | 2 + app/(site)/login/page.tsx | 5 + app/api/auth/login/route.ts | 48 +++++++ app/api/auth/logout/route.ts | 13 ++ app/components/admin/AdminConsole.tsx | 53 ++++++-- app/components/forms/LoginPageClient.tsx | 119 ++++++++++++++++++ app/components/layout/Header/HeaderClient.tsx | 15 +++ app/data/feedback.vi.json | 6 +- app/data/home.vi.json | 4 +- app/data/registration.vi.json | 16 +-- app/data/sponsor.json | 4 +- app/data/sponsor.vi.json | 12 +- middleware.ts | 39 +----- models/User.ts | 18 +++ scripts/seed-admin.ts | 46 +++++++ 15 files changed, 334 insertions(+), 66 deletions(-) create mode 100644 app/(site)/login/page.tsx create mode 100644 app/api/auth/login/route.ts create mode 100644 app/api/auth/logout/route.ts create mode 100644 app/components/forms/LoginPageClient.tsx create mode 100644 models/User.ts create mode 100644 scripts/seed-admin.ts diff --git a/.env.example b/.env.example index 2fc80e3..9480ce3 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ PORT=3000 +# MongoDB — summit request queue (registration + feedback) +MONGODB_URI=mongodb://localhost:27017/ipv6_summit \ No newline at end of file diff --git a/app/(site)/login/page.tsx b/app/(site)/login/page.tsx new file mode 100644 index 0000000..2e1b175 --- /dev/null +++ b/app/(site)/login/page.tsx @@ -0,0 +1,5 @@ +import LoginPageClient from "@/app/components/forms/LoginPageClient"; + +export default function LoginPage() { + return ; +} diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts new file mode 100644 index 0000000..f00b00f --- /dev/null +++ b/app/api/auth/login/route.ts @@ -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 }); + } +} diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts new file mode 100644 index 0000000..7f45a9e --- /dev/null +++ b/app/api/auth/logout/route.ts @@ -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; +} diff --git a/app/components/admin/AdminConsole.tsx b/app/components/admin/AdminConsole.tsx index 8be6a5f..1d84c2c 100644 --- a/app/components/admin/AdminConsole.tsx +++ b/app/components/admin/AdminConsole.tsx @@ -227,6 +227,16 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData; const [detail, setDetail] = useState(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 ( - + {/* Settings button removed as per request */}
@@ -382,10 +388,33 @@ export default function AdminConsole({ data, view = "all" }: { data: AdminData; VIE
-
- - {t.meta.user.role} - +
+ + + {userDropdown && ( +
+ +
+ )}
diff --git a/app/components/forms/LoginPageClient.tsx b/app/components/forms/LoginPageClient.tsx new file mode 100644 index 0000000..456e923 --- /dev/null +++ b/app/components/forms/LoginPageClient.tsx @@ -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 ( +
+
+
+
+ + + arrow_back + + {backLabel} + +
+ +
+

+ {titleLabel} +

+
+ +
+
+ +
+ {error && ( +
+ {error} +
+ )} + +
+ + setEmail(e.target.value)} + /> +
+ +
+ + setPassword(e.target.value)} + /> +
+ +
+ +
+
+
+
+
+
+ ); +} diff --git a/app/components/layout/Header/HeaderClient.tsx b/app/components/layout/Header/HeaderClient.tsx index 7598299..4ea3173 100644 --- a/app/components/layout/Header/HeaderClient.tsx +++ b/app/components/layout/Header/HeaderClient.tsx @@ -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({ + + {loginLabel} + + setMobileOpen(false)} + > + {loginNowLabel} + = 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(); diff --git a/models/User.ts b/models/User.ts new file mode 100644 index 0000000..7cbd795 --- /dev/null +++ b/models/User.ts @@ -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("User", UserSchema); diff --git a/scripts/seed-admin.ts b/scripts/seed-admin.ts new file mode 100644 index 0000000..e08af41 --- /dev/null +++ b/scripts/seed-admin.ts @@ -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();