feat: complete multi-user RBAC integration, login system, and fix backend API crashing

This commit is contained in:
Rafif-Riqullah-Siregar committed 2026-07-01 11:47:17 +07:00
1 parent 4aa12511e8
commit 72ded25e4d
31 files changed
+954 -411

No files matched your search

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+9
View File
@@ -0,0 +1,9 @@
import { DashboardLayout } from "@/components/layout/DashboardLayout";
export default function AppLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return <DashboardLayout>{children}</DashboardLayout>;
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+24 -9
View File
@@ -22,22 +22,37 @@ export async function PATCH(req: NextRequest, props: { params: Promise<{ route:
async function handleProxy(req: NextRequest, { params }: { params: Promise<{ route: string[] }> }) {
try {
const route = (await params).route.join('/');
const baseUrl = process.env.NETIFY_BASE_URL || 'https://manager.netify.ai/api/v1';
const routeArray = (await params).route;
const route = routeArray.join('/');
// Construct the destination URL including search parameters
let destinationUrl = '';
const url = new URL(req.url);
const destinationUrl = `${baseUrl}/${route}${url.search}`;
if (routeArray[0] === 'auth' || routeArray[0] === 'dashboard') {
const localBase = process.env.NEXT_PUBLIC_API_URL || 'http://127.0.0.1:3001';
destinationUrl = `${localBase}/api/${route}${url.search}`;
} else {
const baseUrl = process.env.NETIFY_BASE_URL || 'https://manager.netify.ai/api/v1';
destinationUrl = `${baseUrl}/${route}${url.search}`;
}
// Prepare headers
const headers = new Headers();
headers.set('Content-Type', 'application/json');
if (process.env.NETIFY_JWT_TOKEN && process.env.NETIFY_SITE_UUID) {
headers.set('x-api-key', process.env.NETIFY_JWT_TOKEN);
headers.set('x-net-site', process.env.NETIFY_SITE_UUID);
} else if (process.env.NETIFY_API_KEY) {
headers.set('x-api-key', process.env.NETIFY_API_KEY);
if (routeArray[0] !== 'auth' && routeArray[0] !== 'dashboard') {
if (process.env.NETIFY_JWT_TOKEN && process.env.NETIFY_SITE_UUID) {
headers.set('x-api-key', process.env.NETIFY_JWT_TOKEN);
headers.set('x-net-site', process.env.NETIFY_SITE_UUID);
} else if (process.env.NETIFY_API_KEY) {
headers.set('x-api-key', process.env.NETIFY_API_KEY);
}
} else {
// For local auth/dashboard routes, pass along the cookie for authentication
const cookie = req.headers.get('cookie');
if (cookie) {
headers.set('cookie', cookie);
}
}
// Some requests like GET cannot have a body
+4 -5
View File
@@ -1,16 +1,14 @@
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import { DashboardLayout } from "@/components/layout/DashboardLayout";
const inter = Inter({
variable: "--font-inter",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Netify Dashboard",
description: "Netify Network Intelligence Dashboard",
title: "BackOne Dashboard",
description: "BackOne by PT. Data Bisnis Solusi - Network Intelligence Dashboard",
};
export default function RootLayout({
@@ -22,9 +20,10 @@ export default function RootLayout({
<html
lang="en"
className={`${inter.variable} h-full antialiased dark`}
suppressHydrationWarning
>
<body className="min-h-full flex flex-col">
<DashboardLayout>{children}</DashboardLayout>
{children}
</body>
</html>
);
+108
View File
@@ -0,0 +1,108 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { Lock, User } from "lucide-react";
import Image from "next/image";
export default function LoginPage() {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
try {
const res = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || "Login failed");
}
router.push("/");
router.refresh(); // Refresh to update middleware state
} catch (err: any) {
setError(err.message);
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-background flex flex-col justify-center items-center relative overflow-hidden">
{/* Background Glows */}
<div className="absolute top-[-10%] left-[-10%] w-[40%] h-[40%] rounded-full bg-red-600/20 blur-[120px] pointer-events-none" />
<div className="absolute bottom-[-10%] right-[-10%] w-[40%] h-[40%] rounded-full bg-blue-700/20 blur-[120px] pointer-events-none" />
<div className="z-10 w-full max-w-md p-8 bg-sidebar/80 backdrop-blur-2xl rounded-2xl border border-border/50 shadow-2xl relative">
<div className="flex flex-col items-center mb-8">
<img src="/backone-logo.svg" alt="BackOne" className="w-16 h-16 drop-shadow-[0_0_15px_rgba(220,38,38,0.5)] mb-4" />
<h1 className="text-2xl font-bold text-white tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-white/70">
BackOne Dashboard
</h1>
<p className="text-sm text-muted-foreground mt-2">Sign in to your account</p>
</div>
{error && (
<div className="mb-6 p-3 rounded-lg bg-red-500/10 border border-red-500/50 text-red-500 text-sm text-center">
{error}
</div>
)}
<form onSubmit={handleLogin} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium text-slate-300">Username</label>
<div className="relative">
<User className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all"
placeholder="admin"
required
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-300">Password</label>
<div className="relative">
<Lock className="absolute left-3 top-2.5 h-5 w-5 text-muted-foreground" />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 bg-background border border-border rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary/50 transition-all"
placeholder="••••••••"
required
/>
</div>
</div>
<button
type="submit"
disabled={loading}
className="w-full py-2.5 px-4 bg-gradient-to-r from-red-600 to-blue-700 hover:from-red-500 hover:to-blue-600 text-white font-medium rounded-lg shadow-[0_0_15px_rgba(220,38,38,0.3)] hover:shadow-[0_0_20px_rgba(220,38,38,0.5)] transition-all duration-300 flex items-center justify-center mt-6"
>
{loading ? (
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
"Sign In"
)}
</button>
</form>
</div>
</div>
);
}
+26 -6
View File
@@ -12,10 +12,11 @@ import {
Zap,
Radio,
Lock,
Search
Search,
LogOut
} from "lucide-react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
const navigation = [
{ name: "Summary", href: "/", icon: LayoutDashboard },
@@ -37,6 +38,17 @@ const navigation = [
export function Sidebar() {
const pathname = usePathname();
const router = useRouter();
const handleLogout = async () => {
try {
await fetch('/api/auth/logout', { method: 'POST' });
router.push('/login');
router.refresh();
} catch (e) {
console.error(e);
}
};
return (
<div className="flex h-full w-64 flex-col border-r border-border/50 bg-sidebar/80 backdrop-blur-2xl relative">
@@ -44,11 +56,9 @@ export function Sidebar() {
<div className="absolute inset-0 bg-gradient-to-b from-primary/5 via-transparent to-transparent pointer-events-none" />
<div className="flex h-[72px] shrink-0 items-center px-6 border-b border-border/50 relative z-10">
<div className="relative flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10 border border-primary/20 mr-3 shadow-[0_0_15px_rgba(59,130,246,0.2)]">
<Shield className="h-5 w-5 text-primary" />
</div>
<img src="/backone-logo.svg" alt="BackOne Logo" className="w-8 h-8 mr-3 rounded-full drop-shadow-[0_0_10px_rgba(255,0,0,0.3)]" />
<span className="text-lg font-bold text-white tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-white/70">
Netify
BackOne
</span>
</div>
<div className="flex flex-1 flex-col overflow-y-auto relative z-10">
@@ -91,6 +101,16 @@ export function Sidebar() {
})}
</nav>
</div>
<div className="p-4 border-t border-border/50 relative z-10 shrink-0">
<button
onClick={handleLogout}
className="flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-medium text-red-400 hover:bg-red-500/10 hover:text-red-300 transition-all duration-200"
>
<LogOut className="mr-3 h-5 w-5" />
Logout
</button>
</div>
</div>
);
}
+33
View File
@@ -0,0 +1,33 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value;
const { pathname } = request.nextUrl;
const isAuthPage = pathname.startsWith('/login');
// Protect all pages except /login and static assets/api
// The actual API routes inside /api are protected by Express backend (or Next.js API routes)
// We don't intercept /api requests here to avoid breaking them, but we protect the UI pages
if (pathname.startsWith('/api') || pathname.startsWith('/_next') || pathname === '/favicon.ico' || pathname.endsWith('.svg')) {
return NextResponse.next();
}
if (!token && !isAuthPage) {
const loginUrl = new URL('/login', request.url);
return NextResponse.redirect(loginUrl);
}
if (token && isAuthPage) {
const dashboardUrl = new URL('/', request.url);
return NextResponse.redirect(dashboardUrl);
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};