diff --git a/frontend/app/(protected)/_components/navigation.jsx b/frontend/app/(protected)/_components/navigation.jsx new file mode 100644 index 00000000..4a7bcab3 --- /dev/null +++ b/frontend/app/(protected)/_components/navigation.jsx @@ -0,0 +1,85 @@ +//File: frontend/app/(protected)/_components/navigation.jsx +'use client'; // <-- 1. กำหนดให้ไฟล์นี้เป็น Client Component + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { Home, FileText, Settings, Package2 } from 'lucide-react'; +import { can } from "@/lib/rbac"; +import { cn } from "@/lib/utils"; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; + + +export function Navigation({ user }) { // 2. รับข้อมูล user มาจาก props + const pathname = usePathname(); // 3. ใช้งาน usePathname ได้แล้ว + + const navLinks = [ + { href: '/dashboard', label: 'Dashboard', icon: Home }, + { href: '/correspondences', label: 'Correspondences', icon: FileText }, + { href: '/drawings', label: 'Drawings', icon: FileText }, + // ... เพิ่มเมนูอื่นๆ ตามต้องการ + ]; + + const adminLink = { + href: '/admin/users', + label: 'Admin', + icon: Settings, + requiredPermission: 'manage_users' + }; + + return ( +
+
+ + + LCB P3 DMS + + {/* Bell Icon can be here if needed */} +
+
+ +
+
+ + + Need Help? + Contact support for any issues or questions. + + + + + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/(protected)/layout.jsx b/frontend/app/(protected)/layout.jsx index b12e1c0d..f70acf32 100644 --- a/frontend/app/(protected)/layout.jsx +++ b/frontend/app/(protected)/layout.jsx @@ -1,85 +1,91 @@ // frontend/app/(protected)/layout.jsx -import Link from "next/link"; +// frontend/app/(protected)/layout.jsx + +import { cookies, headers } from "next/headers"; // 1. ยังคงใช้ฟังก์ชันฝั่ง Server import { redirect } from "next/navigation"; -import { usePathname } from 'next/navigation'; -import { cookies, headers } from "next/headers"; -import { can } from "@/lib/rbac"; -import { Home, FileText, Users, Settings } from 'lucide-react'; // เพิ่ม Users, Settings หรือไอคอนที่ต้องการ +import { Bell, Users } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; + +// 2. Import Navigation Component ที่เราสร้างขึ้นมาใหม่ +import { Navigation } from "./_components/navigation"; export const metadata = { title: "DMS | Protected" }; -const API_BASE = (process.env.NEXT_PUBLIC_API_BASE || "").replace(/\/$/, ""); +const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3001"; -async function fetchSessionFromAPI() { - const cookieStore = await cookies(); // ✅ ต้อง await - const cookieHeader = cookieStore.toString(); +async function fetchSession() { + const cookieStore = cookies(); + const token = cookieStore.get("access_token")?.value; - const hdrs = await headers(); // ✅ ต้อง await - const hostHdr = hdrs.get("host"); - const protoHdr = hdrs.get("x-forwarded-proto") || "https"; + if (!token) return null; - const res = await fetch(`${API_BASE}/api/auth/me`, { - method: "GET", - headers: { - Cookie: cookieHeader, - "X-Forwarded-Host": hostHdr || "", - "X-Forwarded-Proto": protoHdr, - Accept: "application/json", - }, - cache: "no-store", - }); - - if (!res.ok) return null; try { - const data = await res.json(); - return data?.ok ? data : null; - } catch { + const res = await fetch(`${API_BASE}/api/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + cache: "no-store", + }); + + if (!res.ok) return null; + return await res.json(); + } catch (error) { + console.error("Failed to fetch session:", error); return null; } } + export default async function ProtectedLayout({ children }) { - const session = await fetchSessionFromAPI(); - if (!session) { - redirect("/login?next=/dashboard"); + // 3. ดึงข้อมูล Session บน Server + const session = await fetchSession(); + + // ถ้าไม่มี session หรือ user ให้ redirect ไปหน้า login + if (!session?.user) { + redirect("/login"); } - const { user } = session; return ( -
-
+ +
+
+ {/* Mobile navigation can be here */} +
+ {/* Optional: Add a search bar */} +
+ + + + + + {session.user.username || 'My Account'} + + Settings + + {/* Logout button in client-side auth context handles the action */} + Logout + + +
+
+ {children} +
+
+ ); -} +} \ No newline at end of file diff --git a/frontend/lib/auth copy.js b/frontend/lib/auth copy.js new file mode 100644 index 00000000..ee13fb2e --- /dev/null +++ b/frontend/lib/auth copy.js @@ -0,0 +1,38 @@ +// frontend/lib/auth.js +import { cookies } from "next/headers"; + +const COOKIE_NAME = "access_token"; + +/** + * Server-side session fetcher (ใช้ใน Server Components/Layouts) + * - อ่านคุกกี้แบบ async: await cookies() + * - ถ้าไม่มี token → return null + * - ถ้ามี → เรียก /api/auth/me ที่ backend เพื่อตรวจสอบ + */ +export async function getSession() { + // ✅ ต้อง await + const cookieStore = await cookies(); + const token = cookieStore.get(COOKIE_NAME)?.value; + + if (!token) return null; + + // เรียก backend ตรวจ session (ปรับ endpoint ให้ตรงของคุณ) + const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/me`, { + // ส่งต่อคุกกี้ไป backend (เลือกอย่างใดอย่างหนึ่ง) + // วิธี A: ส่ง header Cookie โดยตรง + headers: { Cookie: `${COOKIE_NAME}=${token}` }, + // วิธี B: ถ้า proxy ผ่าน nginx ในโดเมนเดียวกัน ใช้ credentials รวมคุกกี้อัตโนมัติได้ + // credentials: "include", + cache: "no-store", + }); + + if (!res.ok) return null; + + const data = await res.json(); + // คาดหวังโครงสร้าง { user, permissions } จาก backend + return { + user: data.user, + permissions: data.permissions || [], + token, + }; +} diff --git a/frontend/lib/auth.js b/frontend/lib/auth.js index ee13fb2e..e6a0f2bb 100644 --- a/frontend/lib/auth.js +++ b/frontend/lib/auth.js @@ -1,38 +1,82 @@ // frontend/lib/auth.js -import { cookies } from "next/headers"; +// frontend/lib/auth.js + +'use client'; + +import { createContext, useState, useContext, useEffect } from 'react'; +import api from './api'; +// 1. Import cookieDriver ที่คุณมีอยู่แล้ว ซึ่งเป็นวิธีที่ถูกต้อง +import { cookieDriver } from '@/app/_auth/drivers/cookieDriver'; + +const AuthContext = createContext(null); const COOKIE_NAME = "access_token"; -/** - * Server-side session fetcher (ใช้ใน Server Components/Layouts) - * - อ่านคุกกี้แบบ async: await cookies() - * - ถ้าไม่มี token → return null - * - ถ้ามี → เรียก /api/auth/me ที่ backend เพื่อตรวจสอบ - */ -export async function getSession() { - // ✅ ต้อง await - const cookieStore = await cookies(); - const token = cookieStore.get(COOKIE_NAME)?.value; +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); - if (!token) return null; + useEffect(() => { + const initializeAuth = async () => { + // 2. อ่าน token จาก cookie ด้วย cookieDriver.get() + const token = cookieDriver.get(COOKIE_NAME); + + if (token) { + try { + api.defaults.headers.Authorization = `Bearer ${token}`; + // สมมติว่ามี endpoint /auth/me สำหรับดึงข้อมูลผู้ใช้ + const response = await api.get('/auth/me'); + setUser(response.data.user || response.data); // รองรับทั้งสองรูปแบบ + } catch (error) { + console.error("Failed to initialize auth from cookie:", error); + cookieDriver.remove(COOKIE_NAME); + delete api.defaults.headers.Authorization; + } + } + setLoading(false); + }; - // เรียก backend ตรวจ session (ปรับ endpoint ให้ตรงของคุณ) - const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE}/api/auth/me`, { - // ส่งต่อคุกกี้ไป backend (เลือกอย่างใดอย่างหนึ่ง) - // วิธี A: ส่ง header Cookie โดยตรง - headers: { Cookie: `${COOKIE_NAME}=${token}` }, - // วิธี B: ถ้า proxy ผ่าน nginx ในโดเมนเดียวกัน ใช้ credentials รวมคุกกี้อัตโนมัติได้ - // credentials: "include", - cache: "no-store", - }); + initializeAuth(); + }, []); - if (!res.ok) return null; - - const data = await res.json(); - // คาดหวังโครงสร้าง { user, permissions } จาก backend - return { - user: data.user, - permissions: data.permissions || [], - token, + const login = async (credentials) => { + const response = await api.post('/auth/login', credentials); + const { token, user } = response.data; + + // 3. ตั้งค่า token ใน cookie ด้วย cookieDriver.set() + cookieDriver.set(COOKIE_NAME, token, { expires: 7, secure: true, sameSite: 'strict' }); + api.defaults.headers.Authorization = `Bearer ${token}`; + setUser(user); + return user; }; + + const logout = () => { + // 4. ลบ token ออกจาก cookie ด้วย cookieDriver.remove() + cookieDriver.remove(COOKIE_NAME); + delete api.defaults.headers.Authorization; + setUser(null); + window.location.href = '/login'; + }; + + const value = { + user, + isAuthenticated: !!user, + loading, + login, + logout + }; + + return ( + + {!loading && children} + + ); } + +export const useAuth = () => { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; +}; \ No newline at end of file diff --git a/generate-shadcn-components.yml b/generate-shadcn-components.yml index 48d1a9d0..f9e49e32 100644 --- a/generate-shadcn-components.yml +++ b/generate-shadcn-components.yml @@ -12,7 +12,7 @@ services: echo '🎨 Initializing shadcn/ui...' && npx shadcn@latest init -y -d && echo '📥 Adding components...' && - npx shadcn@latest add -y button label input card badge tabs progress dropdown-menu tooltip switch && + npx shadcn@latest add -y alert-dialog dialog checkbox scroll-area button label input card badge tabs progress dropdown-menu tooltip switch && echo '✅ Done! Check components/ui/ directory' " @@ -23,4 +23,4 @@ services: # หลังจากนั้น commit ไฟล์เหล่านี้: # - components/ui/*.tsx (หรือ .jsx) # - lib/utils.ts (หรือ .js) -# - components.json \ No newline at end of file +# - components.json