fronted แก้ layout build dev&proc

This commit is contained in:
admin
2025-10-05 10:57:54 +07:00
parent 02e509986b
commit 5dec188744
5 changed files with 270 additions and 97 deletions

View File

@@ -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 (
<div className="flex h-full max-h-screen flex-col gap-2">
<div className="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
<Link href="/dashboard" className="flex items-center gap-2 font-semibold">
<Package2 className="h-6 w-6" />
<span className="">LCB P3 DMS</span>
</Link>
{/* Bell Icon can be here if needed */}
</div>
<div className="flex-1">
<nav className="grid items-start px-2 text-sm font-medium lg:px-4">
{navLinks.map(({ href, label, icon: Icon }) => (
<Link
key={href}
href={href}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary',
pathname.startsWith(href) ? 'bg-muted text-primary' : ''
)}
>
<Icon className="h-4 w-4" />
{label}
</Link>
))}
{user && can(user, adminLink.requiredPermission) && (
<>
<div className="my-2 border-t"></div>
<Link
href={adminLink.href}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-muted-foreground transition-all hover:text-primary',
pathname.startsWith('/admin') ? 'bg-muted text-primary' : ''
)}
>
<adminLink.icon className="h-4 w-4" />
{adminLink.label}
</Link>
</>
)}
</nav>
</div>
<div className="mt-auto p-4">
<Card>
<CardHeader className="p-2 pt-0 md:p-4">
<CardTitle>Need Help?</CardTitle>
<CardDescription>Contact support for any issues or questions.</CardDescription>
</CardHeader>
<CardContent className="p-2 pt-0 md:p-4 md:pt-0">
<Button size="sm" className="w-full">Contact</Button>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@@ -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 (
<section className="grid grid-cols-12 gap-6 p-4 mx-auto max-w-7xl">
<aside className="col-span-12 lg:col-span-3 xl:col-span-3">
<div className="p-4 border rounded-3xl bg-white/70">
<div className="mb-3 text-sm">RBAC: <b>{user.role}</b></div>
<nav className="space-y-2">
<Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/dashboard">แดชบอร</Link>
<Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/drawings">Drawings</Link>
<Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/rfas">RFAs</Link>
<Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/transmittals">Transmittals</Link>
<Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/correspondences">Correspondences</Link>
<Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/contracts-volumes">Contracts & Volumes</Link>
<Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/reports">Reports</Link>
{can(user, "workflow:view") && <Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/workflow">Workflow (n8n)</Link>}
{can(user, "health:view") && <Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/health">Health</Link>}
{can(user, "users:manage") && <Link className="block px-4 py-2 rounded-xl bg-white/60 hover:bg-white" href="/users">ใช/บทบาท</Link>}
</nav>
</div>
<div className="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
<aside className="hidden border-r bg-muted/40 md:block">
{/* 4. ใช้ Navigation Component และส่งข้อมูล user เป็น props */}
<Navigation user={session.user} />
</aside>
<main className="col-span-12 space-y-6 lg:col-span-9 xl:col-span-9">
<div className="flex items-center gap-2">
<div className="flex-1 text-lg font-semibold">Document Management System LCBP3 Phase 3</div>
{can(user, "admin:view") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/admin">Admin</a>}
{can(user, "users:manage") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/users">ใช/บทบาท</a>}
{can(user, "health:view") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/health">Health</a>}
{can(user, "workflow:view") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/workflow">Workflow</a>}
{can(user, "rfa:create") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/rfas/new">+ RFA</a>}
{can(user, "drawing:upload") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/drawings/upload">+ Upload Drawing</a>}
{can(user, "transmittal:create") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/transmittals/new">+ Transmittal</a>}
{can(user, "correspondence:create") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/correspondences/new">+ หนงสอสอสาร</a>}
</div>
{children}
</main>
</section>
<div className="flex flex-col">
<header className="flex h-14 items-center gap-4 border-b bg-muted/40 px-4 lg:h-[60px] lg:px-6">
{/* Mobile navigation can be here */}
<div className="flex-1 w-full">
{/* Optional: Add a search bar */}
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" className="rounded-full">
<Users className="w-5 h-5" />
<span className="sr-only">Toggle user menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{session.user.username || 'My Account'}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
{/* Logout button in client-side auth context handles the action */}
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</header>
<main className="flex-1 p-4 lg:p-6">
{children}
</main>
</div>
</div>
);
}
}

38
frontend/lib/auth copy.js Normal file
View File

@@ -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,
};
}

View File

@@ -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 (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View File

@@ -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
# - components.json