feat: complete multi-user RBAC integration, login system, and fix backend API crashing
This commit is contained in:
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.
@@ -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.
File renamed without changes.
File renamed without changes.
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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).*)'],
|
||||
};
|
||||
Reference in new issue
Block a user