260218:1712 20260218 TASK-BEFE-001n
All checks were successful
Build and Deploy / deploy (push) Successful in 4m55s

This commit is contained in:
admin
2026-02-18 17:12:11 +07:00
parent 01ce68acda
commit b84284f8a9
54 changed files with 1307 additions and 339 deletions

View File

@@ -0,0 +1,119 @@
'use client';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { sessionService } from '@/lib/services/session.service';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { toast } from 'sonner';
import { Loader2, Trash2, Monitor, Smartphone } from 'lucide-react';
import { format } from 'date-fns';
export default function SessionManagementPage() {
const queryClient = useQueryClient();
const {
data: sessions,
isLoading,
error,
} = useQuery({
queryKey: ['sessions'],
queryFn: sessionService.getActiveSessions,
});
const revokeMutation = useMutation({
mutationFn: sessionService.revokeSession,
onSuccess: () => {
toast.success('Session revoked successfully');
queryClient.invalidateQueries({ queryKey: ['sessions'] });
},
onError: (error) => {
toast.error('Failed to revoke session');
console.error(error);
},
});
const handleRevoke = (id: number) => {
if (confirm('Are you sure you want to revoke this session?')) {
revokeMutation.mutate(id);
}
};
if (isLoading) {
return (
<div className="flex h-[400px] w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (error) {
return <div className="p-8 text-center text-red-500">Failed to load sessions. Please try again.</div>;
}
return (
<div className="space-y-6 p-6">
<div className="flex flex-col gap-2">
<h1 className="text-2xl font-bold tracking-tight">Active Sessions</h1>
<p className="text-sm text-muted-foreground">Monitor and manage active user sessions across all devices.</p>
</div>
<div className="rounded-md border bg-card">
<Table>
<TableHeader>
<TableRow>
<TableHead>User</TableHead>
<TableHead>Device Info</TableHead>
<TableHead>Last Active</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sessions?.map((session: any) => (
<TableRow key={session.id}>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">{session.user.username}</span>
<span className="text-xs text-muted-foreground">
{session.user.firstName} {session.user.lastName}
</span>
</div>
</TableCell>
<TableCell>
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2 text-sm">
<Monitor className="h-3 w-3" />
{session.deviceName || 'Unknown Device'}
</div>
<span className="text-xs text-muted-foreground">{session.ipAddress || 'Unknown IP'}</span>
</div>
</TableCell>
<TableCell className="text-sm">
{session.lastActive ? format(new Date(session.lastActive), 'PP pp') : '-'}
</TableCell>
<TableCell className="text-right">
<Button
variant="destructive"
size="sm"
className="h-8"
onClick={() => handleRevoke(Number(session.id))}
disabled={revokeMutation.isPending}
>
<Trash2 className="mr-2 h-3.5 w-3.5" />
Revoke
</Button>
</TableCell>
</TableRow>
))}
{(!sessions || sessions.length === 0) && (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center text-muted-foreground">
No active sessions found.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
);
}

View File

@@ -1,21 +1,12 @@
"use client";
'use client';
import { useOrganizations } from "@/hooks/use-master-data";
import { useUsers } from "@/hooks/use-users";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Users,
Building2,
FileText,
Settings,
Shield,
Activity,
ArrowRight,
FileStack,
} from "lucide-react";
import Link from "next/link";
import { Skeleton } from "@/components/ui/skeleton";
import { Button } from "@/components/ui/button";
import { useOrganizations } from '@/hooks/use-master-data';
import { useUsers } from '@/hooks/use-users';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Users, Building2, FileText, Settings, Shield, Activity, ArrowRight, FileStack } from 'lucide-react';
import Link from 'next/link';
import { Skeleton } from '@/components/ui/skeleton';
import { Button } from '@/components/ui/button';
export default function AdminPage() {
const { data: organizations, isLoading: orgsLoading } = useOrganizations();
@@ -23,66 +14,66 @@ export default function AdminPage() {
const stats = [
{
title: "Total Users",
title: 'Total Users',
value: users?.length || 0,
icon: Users,
loading: usersLoading,
href: "/admin/users",
color: "text-blue-600",
href: '/admin/access-control/users',
color: 'text-blue-600',
},
{
title: "Organizations",
title: 'Organizations',
value: organizations?.length || 0,
icon: Building2,
loading: orgsLoading,
href: "/admin/organizations",
color: "text-green-600",
href: '/admin/access-control/organizations',
color: 'text-green-600',
},
{
title: "System Logs",
value: "View",
icon: Activity,
loading: false,
href: "/admin/system-logs",
color: "text-orange-600",
}
title: 'System Logs',
value: 'View',
icon: Activity,
loading: false,
href: '/admin/monitoring/system-logs',
color: 'text-orange-600',
},
];
const quickLinks = [
{
title: "User Management",
description: "Manage system users, roles, and permissions",
href: "/admin/users",
title: 'User Management',
description: 'Manage system users, roles, and permissions',
href: '/admin/access-control/users',
icon: Users,
},
{
title: "Organizations",
description: "Manage project organizations and companies",
href: "/admin/organizations",
title: 'Organizations',
description: 'Manage project organizations and companies',
href: '/admin/access-control/organizations',
icon: Building2,
},
{
title: "Workflow Config",
description: "Configure document approval workflows",
href: "/admin/workflows",
title: 'Workflow Config',
description: 'Configure document approval workflows',
href: '/admin/doc-control/workflows',
icon: FileText,
},
{
title: "Security & RBAC",
description: "Configure roles, permissions, and security settings",
href: "/admin/security/roles",
title: 'Security & RBAC',
description: 'Configure roles, permissions, and security settings',
href: '/admin/access-control/roles',
icon: Shield,
},
{
title: "Numbering System",
description: "Setup document numbering templates",
href: "/admin/numbering",
title: 'Numbering System',
description: 'Setup document numbering templates',
href: '/admin/doc-control/numbering',
icon: Settings,
},
{
title: "Drawing Master Data",
description: "Manage drawing categories, volumes, and classifications",
href: "/admin/drawings",
title: 'Drawing Master Data',
description: 'Manage drawing categories, volumes, and classifications',
href: '/admin/doc-control/drawings',
icon: FileStack,
},
];
@@ -91,18 +82,14 @@ export default function AdminPage() {
<div className="space-y-8 p-8">
<div>
<h1 className="text-3xl font-bold tracking-tight">Admin Dashboard</h1>
<p className="text-muted-foreground mt-2">
System overview and quick access to administrative functions.
</p>
<p className="text-muted-foreground mt-2">System overview and quick access to administrative functions.</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{stats.map((stat, index) => (
<Card key={index} className="hover:shadow-md transition-shadow">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">
{stat.title}
</CardTitle>
<CardTitle className="text-sm font-medium">{stat.title}</CardTitle>
<stat.icon className={`h-4 w-4 ${stat.color}`} />
</CardHeader>
<CardContent>
@@ -112,10 +99,7 @@ export default function AdminPage() {
<div className="text-2xl font-bold">{stat.value}</div>
)}
{stat.href && (
<Link
href={stat.href}
className="text-xs text-muted-foreground hover:underline mt-1 inline-block"
>
<Link href={stat.href} className="text-xs text-muted-foreground hover:underline mt-1 inline-block">
View details
</Link>
)}
@@ -137,9 +121,7 @@ export default function AdminPage() {
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
{link.description}
</p>
<p className="text-sm text-muted-foreground">{link.description}</p>
<Button variant="ghost" className="mt-4 p-0 h-auto font-normal text-primary hover:no-underline group">
Go to module <ArrowRight className="ml-1 h-3 w-3 group-hover:translate-x-1 transition-transform" />
</Button>

View File

@@ -1,131 +0,0 @@
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import apiClient from "@/lib/api/client";
import { DataTable } from "@/components/common/data-table";
import { ColumnDef } from "@tanstack/react-table";
import { Button } from "@/components/ui/button";
import { LogOut, Monitor, Smartphone, RefreshCw } from "lucide-react";
import { format } from "date-fns";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
interface Session {
id: string;
userId: number;
user: {
username: string;
firstName: string;
lastName: string;
};
deviceName: string; // e.g., "Chrome on Windows"
ipAddress: string;
lastActive: string;
isCurrent: boolean;
}
const sessionService = {
getAll: async () => {
const response = await apiClient.get("/auth/sessions");
return response.data.data || response.data;
},
revoke: async (sessionId: string) => (await apiClient.delete(`/auth/sessions/${sessionId}`)).data,
};
export default function SessionsPage() {
const queryClient = useQueryClient();
const { data: sessions = [], isLoading } = useQuery<Session[]>({
queryKey: ["sessions"],
queryFn: sessionService.getAll,
});
const revokeMutation = useMutation({
mutationFn: sessionService.revoke,
onSuccess: () => {
toast.success("Session revoked successfully");
queryClient.invalidateQueries({ queryKey: ["sessions"] });
},
onError: () => toast.error("Failed to revoke session"),
});
const columns: ColumnDef<Session>[] = [
{
accessorKey: "user",
header: "User",
cell: ({ row }) => {
const user = row.original.user;
return (
<div className="flex flex-col">
<span className="font-medium">{user.username}</span>
<span className="text-xs text-muted-foreground">
{user.firstName} {user.lastName}
</span>
</div>
);
},
},
{
accessorKey: "deviceName",
header: "Device / IP",
cell: ({ row }) => (
<div className="flex items-center gap-2">
{row.original.deviceName.toLowerCase().includes("mobile") ? (
<Smartphone className="h-4 w-4 text-muted-foreground" />
) : (
<Monitor className="h-4 w-4 text-muted-foreground" />
)}
<div className="flex flex-col">
<span>{row.original.deviceName}</span>
<span className="text-xs text-muted-foreground">{row.original.ipAddress}</span>
</div>
</div>
),
},
{
accessorKey: "lastActive",
header: "Last Active",
cell: ({ row }) => format(new Date(row.original.lastActive), "dd MMM yyyy, HH:mm"),
},
{
id: "status",
header: "Status",
cell: ({ row }) =>
row.original.isCurrent ? <Badge>Current</Badge> : <Badge variant="secondary">Active</Badge>,
},
{
id: "actions",
cell: ({ row }) => (
<Button
variant="destructive"
size="sm"
disabled={row.original.isCurrent || revokeMutation.isPending}
onClick={() => revokeMutation.mutate(row.original.id)}
>
<LogOut className="h-4 w-4 mr-2" />
Revoke
</Button>
),
},
];
if (isLoading) {
return (
<div className="flex justify-center p-12">
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="p-6 space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold">Active Sessions</h1>
<p className="text-muted-foreground">Manage user sessions and force logout if needed</p>
</div>
</div>
<DataTable columns={columns} data={sessions} />
</div>
);
}

View File

@@ -1,28 +1,24 @@
import { AdminSidebar } from "@/components/admin/sidebar";
import { AdminSidebar } from '@/components/admin/sidebar';
import { auth } from "@/lib/auth";
import { auth } from '@/lib/auth';
import { redirect } from 'next/navigation';
export default async function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
const session = await auth();
// Temporary bypass for UI testing
const isAdmin = true; // session?.user?.role === 'ADMIN';
// Validate Admin or DC role
const userRole = session?.user?.role;
const isAdmin = userRole === 'ADMIN' || userRole === 'DC';
if (!session || !isAdmin) {
// redirect("/");
redirect('/dashboard'); // Redirect unauthorized users to dashboard
}
return (
<div className="flex h-screen w-full bg-background">
<AdminSidebar />
<div className="flex-1 overflow-auto bg-muted/10 p-4">
{children}
</div>
<div className="flex-1 overflow-auto bg-muted/10 p-4">{children}</div>
</div>
);
}