fronted แก้ layout build dev&proc
This commit is contained in:
85
frontend/app/(protected)/_components/navigation.jsx
Normal file
85
frontend/app/(protected)/_components/navigation.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,85 +1,91 @@
|
|||||||
// frontend/app/(protected)/layout.jsx
|
// 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 { redirect } from "next/navigation";
|
||||||
import { usePathname } from 'next/navigation';
|
import { Bell, Users } from 'lucide-react';
|
||||||
import { cookies, headers } from "next/headers";
|
|
||||||
import { can } from "@/lib/rbac";
|
import { Button } from '@/components/ui/button';
|
||||||
import { Home, FileText, Users, Settings } from 'lucide-react'; // เพิ่ม Users, Settings หรือไอคอนที่ต้องการ
|
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" };
|
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() {
|
async function fetchSession() {
|
||||||
const cookieStore = await cookies(); // ✅ ต้อง await
|
const cookieStore = cookies();
|
||||||
const cookieHeader = cookieStore.toString();
|
const token = cookieStore.get("access_token")?.value;
|
||||||
|
|
||||||
const hdrs = await headers(); // ✅ ต้อง await
|
if (!token) return null;
|
||||||
const hostHdr = hdrs.get("host");
|
|
||||||
const protoHdr = hdrs.get("x-forwarded-proto") || "https";
|
|
||||||
|
|
||||||
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 {
|
try {
|
||||||
const data = await res.json();
|
const res = await fetch(`${API_BASE}/api/auth/me`, {
|
||||||
return data?.ok ? data : null;
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
} catch {
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) return null;
|
||||||
|
return await res.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch session:", error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default async function ProtectedLayout({ children }) {
|
export default async function ProtectedLayout({ children }) {
|
||||||
const session = await fetchSessionFromAPI();
|
// 3. ดึงข้อมูล Session บน Server
|
||||||
if (!session) {
|
const session = await fetchSession();
|
||||||
redirect("/login?next=/dashboard");
|
|
||||||
|
// ถ้าไม่มี session หรือ user ให้ redirect ไปหน้า login
|
||||||
|
if (!session?.user) {
|
||||||
|
redirect("/login");
|
||||||
}
|
}
|
||||||
const { user } = session;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="grid grid-cols-12 gap-6 p-4 mx-auto max-w-7xl">
|
<div className="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
|
||||||
<aside className="col-span-12 lg:col-span-3 xl:col-span-3">
|
<aside className="hidden border-r bg-muted/40 md:block">
|
||||||
<div className="p-4 border rounded-3xl bg-white/70">
|
{/* 4. ใช้ Navigation Component และส่งข้อมูล user เป็น props */}
|
||||||
<div className="mb-3 text-sm">RBAC: <b>{user.role}</b></div>
|
<Navigation user={session.user} />
|
||||||
<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>
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main className="col-span-12 space-y-6 lg:col-span-9 xl:col-span-9">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center gap-2">
|
<header className="flex h-14 items-center gap-4 border-b bg-muted/40 px-4 lg:h-[60px] lg:px-6">
|
||||||
<div className="flex-1 text-lg font-semibold">Document Management System — LCBP3 Phase 3</div>
|
{/* Mobile navigation can be here */}
|
||||||
{can(user, "admin:view") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/admin">Admin</a>}
|
<div className="flex-1 w-full">
|
||||||
{can(user, "users:manage") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/users">ผู้ใช้/บทบาท</a>}
|
{/* Optional: Add a search bar */}
|
||||||
{can(user, "health:view") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/health">Health</a>}
|
</div>
|
||||||
{can(user, "workflow:view") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/workflow">Workflow</a>}
|
<DropdownMenu>
|
||||||
{can(user, "rfa:create") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/rfas/new">+ RFA</a>}
|
<DropdownMenuTrigger asChild>
|
||||||
{can(user, "drawing:upload") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/drawings/upload">+ Upload Drawing</a>}
|
<Button variant="secondary" size="icon" className="rounded-full">
|
||||||
{can(user, "transmittal:create") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/transmittals/new">+ Transmittal</a>}
|
<Users className="w-5 h-5" />
|
||||||
{can(user, "correspondence:create") && <a className="px-3 py-2 text-white rounded-xl" style={{ background: "#0D5C75" }} href="/correspondences/new">+ หนังสือสื่อสาร</a>}
|
<span className="sr-only">Toggle user menu</span>
|
||||||
</div>
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
{children}
|
<DropdownMenuContent align="end">
|
||||||
</main>
|
<DropdownMenuLabel>{session.user.username || 'My Account'}</DropdownMenuLabel>
|
||||||
</section>
|
<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
38
frontend/lib/auth copy.js
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,38 +1,82 @@
|
|||||||
// frontend/lib/auth.js
|
// 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";
|
const COOKIE_NAME = "access_token";
|
||||||
|
|
||||||
/**
|
export function AuthProvider({ children }) {
|
||||||
* Server-side session fetcher (ใช้ใน Server Components/Layouts)
|
const [user, setUser] = useState(null);
|
||||||
* - อ่านคุกกี้แบบ async: await cookies()
|
const [loading, setLoading] = useState(true);
|
||||||
* - ถ้าไม่มี 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;
|
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 ให้ตรงของคุณ)
|
initializeAuth();
|
||||||
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 login = async (credentials) => {
|
||||||
|
const response = await api.post('/auth/login', credentials);
|
||||||
const data = await res.json();
|
const { token, user } = response.data;
|
||||||
// คาดหวังโครงสร้าง { user, permissions } จาก backend
|
|
||||||
return {
|
// 3. ตั้งค่า token ใน cookie ด้วย cookieDriver.set()
|
||||||
user: data.user,
|
cookieDriver.set(COOKIE_NAME, token, { expires: 7, secure: true, sameSite: 'strict' });
|
||||||
permissions: data.permissions || [],
|
api.defaults.headers.Authorization = `Bearer ${token}`;
|
||||||
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;
|
||||||
|
};
|
||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
echo '🎨 Initializing shadcn/ui...' &&
|
echo '🎨 Initializing shadcn/ui...' &&
|
||||||
npx shadcn@latest init -y -d &&
|
npx shadcn@latest init -y -d &&
|
||||||
echo '📥 Adding components...' &&
|
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'
|
echo '✅ Done! Check components/ui/ directory'
|
||||||
"
|
"
|
||||||
|
|
||||||
@@ -23,4 +23,4 @@ services:
|
|||||||
# หลังจากนั้น commit ไฟล์เหล่านี้:
|
# หลังจากนั้น commit ไฟล์เหล่านี้:
|
||||||
# - components/ui/*.tsx (หรือ .jsx)
|
# - components/ui/*.tsx (หรือ .jsx)
|
||||||
# - lib/utils.ts (หรือ .js)
|
# - lib/utils.ts (หรือ .js)
|
||||||
# - components.json
|
# - components.json
|
||||||
|
|||||||
Reference in New Issue
Block a user