diff --git a/dashboard/next.config.ts b/dashboard/next.config.ts index 41a9c18..91749db 100644 --- a/dashboard/next.config.ts +++ b/dashboard/next.config.ts @@ -1,5 +1,12 @@ import type { NextConfig } from "next"; -const nextConfig: NextConfig = {/* config options here */}; +const nextConfig: NextConfig = { + basePath: "/admin", + output: "export", + trailingSlash: true, + images: { + unoptimized: true, + }, +}; export default nextConfig; diff --git a/dashboard/src/app/analytics/page.tsx b/dashboard/src/app/analytics/page.tsx index f2533fb..2aac24f 100644 --- a/dashboard/src/app/analytics/page.tsx +++ b/dashboard/src/app/analytics/page.tsx @@ -26,7 +26,7 @@ export default function AnalyticsPage() { const fetchMetrics = useCallback(async () => { setIsLoading(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/system/metrics`, { headers: { Authorization: `Bearer ${projectApiKey}`, diff --git a/dashboard/src/app/dlq/page.tsx b/dashboard/src/app/dlq/page.tsx index 6581466..a54752c 100644 --- a/dashboard/src/app/dlq/page.tsx +++ b/dashboard/src/app/dlq/page.tsx @@ -34,7 +34,7 @@ export default function DeadLetterQueuePage() { const fetchDLQ = useCallback(async () => { setIsLoading(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/dlq`, { headers: { Authorization: `Bearer ${projectApiKey}`, @@ -60,7 +60,7 @@ export default function DeadLetterQueuePage() { const handleReplay = async (id: string) => { try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/dlq/replay`, { method: "POST", headers: { @@ -80,7 +80,7 @@ export default function DeadLetterQueuePage() { const handleDelete = async (id: string) => { try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/dlq/${id}`, { method: "DELETE", headers: { diff --git a/dashboard/src/app/globals.css b/dashboard/src/app/globals.css index c56032b..6a25b1b 100644 --- a/dashboard/src/app/globals.css +++ b/dashboard/src/app/globals.css @@ -127,4 +127,4 @@ html { @apply font-sans; } -} \ No newline at end of file +} diff --git a/dashboard/src/app/layout.tsx b/dashboard/src/app/layout.tsx index 8b3023a..bbce3d5 100644 --- a/dashboard/src/app/layout.tsx +++ b/dashboard/src/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import { Toaster } from "@/components/ui/sonner"; import Sidebar from "@/components/Sidebar"; +import { AuthProvider } from "@/hooks/useAuth"; import { ProjectProvider } from "@/hooks/useProjectKey"; import "./globals.css"; @@ -16,8 +17,8 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "Notifkit Observability", - description: "Real-time delivery observability dashboard for Notifkit", + title: "Notifkit Observability & Admin", + description: "Real-time delivery observability dashboard and admin console for Notifkit", }; export default function RootLayout({ children }: { children: React.ReactNode }) { @@ -27,11 +28,13 @@ export default function RootLayout({ children }: { children: React.ReactNode }) className={`${geistSans.variable} ${geistMono.variable} h-full antialiased dark`} > - - -
{children}
- -
+ + + +
{children}
+ +
+
); diff --git a/dashboard/src/app/login/page.tsx b/dashboard/src/app/login/page.tsx new file mode 100644 index 0000000..0d2f8ce --- /dev/null +++ b/dashboard/src/app/login/page.tsx @@ -0,0 +1,143 @@ +"use client"; + +import { useState } from "react"; +import { useAuth } from "@/hooks/useAuth"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Bell, Lock, User, Loader2, AlertCircle, ArrowRight, ShieldCheck } from "lucide-react"; +import { toast } from "sonner"; + +export default function LoginPage() { + const { login } = useAuth(); + const [identifier, setIdentifier] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!identifier.trim() || !password) { + setErrorMessage("Please enter both your email/user ID and password."); + return; + } + + setLoading(true); + setErrorMessage(null); + + const res = await login(identifier.trim(), password); + setLoading(false); + + if (res.success) { + toast.success("Welcome back!", { description: "Logged in successfully." }); + } else { + setErrorMessage(res.error || "Invalid email/user ID or password."); + toast.error("Authentication failed", { description: res.error }); + } + }; + + return ( +
+ {/* Background Decorative Gradients */} +
+
+
+ +
+
+
+ +
+

+ Notifkit Admin +

+

+ Sign in to access your notification engine and observability suite. +

+
+ + + + Admin Login + + Enter your admin credentials below + + + + {errorMessage && ( +
+ + {errorMessage} +
+ )} + +
{ + e.preventDefault(); + void handleSubmit(e); + }} + className="space-y-4" + > +
+ +
+ setIdentifier(e.target.value)} + className="w-full rounded-lg border border-border/60 bg-muted/30 px-3.5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all" + /> +
+
+ +
+ +
+ setPassword(e.target.value)} + className="w-full rounded-lg border border-border/60 bg-muted/30 px-3.5 py-2.5 text-sm text-foreground placeholder:text-muted-foreground/50 focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all" + /> +
+
+ + +
+ +
+ + Protected by salted Scrypt & Redis session auth +
+
+
+
+
+ ); +} diff --git a/dashboard/src/app/projects/page.tsx b/dashboard/src/app/projects/page.tsx index f0b7d45..aecdd19 100644 --- a/dashboard/src/app/projects/page.tsx +++ b/dashboard/src/app/projects/page.tsx @@ -6,6 +6,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/com import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { FolderKey, Plus, Copy, Trash2, AlertCircle, Eye, EyeOff } from "lucide-react"; +import { useAuth } from "@/hooks/useAuth"; interface Project { id: string; @@ -21,6 +22,7 @@ interface ApiKey { } export default function ProjectsPage() { + const { apiUrl, token: adminKey } = useAuth(); const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [newProjectName, setNewProjectName] = useState(""); @@ -30,9 +32,6 @@ export default function ProjectsPage() { const [newlyGeneratedKey, setNewlyGeneratedKey] = useState(null); const [showKey, setShowKey] = useState(false); - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; - const adminKey = process.env.NEXT_PUBLIC_ADMIN_KEY || "nk_live_1234567890abcdef1234567890abcdef"; - const fetchProjects = async () => { try { const res = await fetch(`${apiUrl}/v1/projects`, { diff --git a/dashboard/src/app/scheduled/page.tsx b/dashboard/src/app/scheduled/page.tsx index 7f72ea2..021b5ed 100644 --- a/dashboard/src/app/scheduled/page.tsx +++ b/dashboard/src/app/scheduled/page.tsx @@ -31,7 +31,7 @@ export default function ScheduledMessagesPage() { const fetchScheduled = useCallback(async () => { setIsLoading(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/notifications/scheduled`, { headers: { Authorization: `Bearer ${projectApiKey}`, @@ -57,7 +57,7 @@ export default function ScheduledMessagesPage() { const handleCancel = async (taskId: string) => { try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/notifications/${taskId}`, { method: "DELETE", headers: { diff --git a/dashboard/src/app/system/page.tsx b/dashboard/src/app/system/page.tsx index 6b4048a..fde2fbe 100644 --- a/dashboard/src/app/system/page.tsx +++ b/dashboard/src/app/system/page.tsx @@ -42,7 +42,7 @@ export default function SystemHealthPage() { const fetchHealth = useCallback(async () => { setIsLoading(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/system/health`, { headers: { Authorization: `Bearer ${projectApiKey}`, diff --git a/dashboard/src/app/templates/page.tsx b/dashboard/src/app/templates/page.tsx index 6927a47..291ee32 100644 --- a/dashboard/src/app/templates/page.tsx +++ b/dashboard/src/app/templates/page.tsx @@ -36,7 +36,7 @@ export default function TemplatesPage() { const fetchTemplates = useCallback(async () => { setIsLoading(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/templates`, { headers: { Authorization: `Bearer ${projectApiKey}`, @@ -62,7 +62,7 @@ export default function TemplatesPage() { const handleDelete = async (id: string) => { try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/templates/${id}`, { method: "DELETE", headers: { diff --git a/dashboard/src/app/users/page.tsx b/dashboard/src/app/users/page.tsx index 24c919a..0b94ac0 100644 --- a/dashboard/src/app/users/page.tsx +++ b/dashboard/src/app/users/page.tsx @@ -41,7 +41,7 @@ export default function UsersPage() { const fetchUsers = useCallback(async (apiKey: string, projectId: string) => { setLoading(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/users`, { headers: { Authorization: `Bearer ${apiKey}`, @@ -75,7 +75,7 @@ export default function UsersPage() { if (!id) return; setLoadingDetail(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/users/${encodeURIComponent(id)}/details`, { headers: { Authorization: `Bearer ${projectApiKey}`, diff --git a/dashboard/src/app/workflows/page.tsx b/dashboard/src/app/workflows/page.tsx index ebca1cd..c7f346d 100644 --- a/dashboard/src/app/workflows/page.tsx +++ b/dashboard/src/app/workflows/page.tsx @@ -50,7 +50,7 @@ export default function WorkflowsPage() { const fetchWorkflows = useCallback(async () => { setIsLoading(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/workflows`, { headers: { Authorization: `Bearer ${projectApiKey}`, @@ -76,7 +76,7 @@ export default function WorkflowsPage() { const handleTrigger = async (name: string) => { try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/workflows/trigger`, { method: "POST", headers: { @@ -103,7 +103,7 @@ export default function WorkflowsPage() { if (!targetId) return; setSearchingInstance(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const res = await fetch(`${apiUrl}/v1/workflows/instances/${encodeURIComponent(targetId)}`, { headers: { Authorization: `Bearer ${projectApiKey}`, diff --git a/dashboard/src/components/DashboardHeader.tsx b/dashboard/src/components/DashboardHeader.tsx index 76bf1df..9185b08 100644 --- a/dashboard/src/components/DashboardHeader.tsx +++ b/dashboard/src/components/DashboardHeader.tsx @@ -4,16 +4,17 @@ import { useEffect, useState } from "react"; import { Activity, Bell, FolderKey, AlertTriangle } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { useProject } from "@/hooks/useProjectKey"; +import { useAuth } from "@/hooks/useAuth"; export default function DashboardHeader({ isConnected }: { isConnected: boolean }) { const { projects, selectedProjectId, setSelectedProjectId, projectApiKey } = useProject(); + const { apiUrl } = useAuth(); const [highBackpressure, setHighBackpressure] = useState(false); useEffect(() => { if (!projectApiKey || !selectedProjectId) return; const checkMetrics = async () => { try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; const res = await fetch(`${apiUrl}/v1/system/metrics`, { headers: { Authorization: `Bearer ${projectApiKey}`, @@ -34,7 +35,7 @@ export default function DashboardHeader({ isConnected }: { isConnected: boolean void checkMetrics(); const interval = setInterval(() => void checkMetrics(), 10000); return () => clearInterval(interval); - }, [projectApiKey, selectedProjectId]); + }, [apiUrl, projectApiKey, selectedProjectId]); return (
@@ -74,20 +75,30 @@ export default function DashboardHeader({ isConnected }: { isConnected: boolean
)} - - {isConnected ? ( - - - - - ) : ( - - )} - {isConnected ? "Live Stream Active" : "Disconnected"} - + {projects.length === 0 ? ( + + + No Project Created + + ) : ( + + {isConnected ? ( + + + + + ) : ( + + )} + {isConnected ? "Live Stream Active" : "Disconnected"} + + )}
diff --git a/dashboard/src/components/LogsTable.tsx b/dashboard/src/components/LogsTable.tsx index 2c6d069..fc8a6c9 100644 --- a/dashboard/src/components/LogsTable.tsx +++ b/dashboard/src/components/LogsTable.tsx @@ -60,7 +60,7 @@ export default function LogsTable() { async (apiKey: string, projectId: string, cursor?: string | null) => { setIsLoading(true); try { - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const query = new URLSearchParams({ limit: "25" }); if (cursor) query.set("cursor", cursor); if (search) query.set("search", search); @@ -104,7 +104,7 @@ export default function LogsTable() { useEffect(() => { if (selectedLog && projectApiKey && selectedProjectId) { setLoadingLifecycle(true); - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; fetch(`${apiUrl}/v1/notifications/${selectedLog.taskId}`, { headers: { Authorization: `Bearer ${projectApiKey}`, diff --git a/dashboard/src/components/RealTimeFeed.tsx b/dashboard/src/components/RealTimeFeed.tsx index 658a6fd..2510527 100644 --- a/dashboard/src/components/RealTimeFeed.tsx +++ b/dashboard/src/components/RealTimeFeed.tsx @@ -30,9 +30,9 @@ export default function RealTimeFeed({ if (!projectApiKey || !selectedProjectId) return; setIsConnected(false); - const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:3000"; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || ""; const es = new EventSource( - `${apiUrl}/v1/events/stream?token=${projectApiKey}&projectId=${selectedProjectId}`, + `${apiUrl}/v1/events/stream?token=${encodeURIComponent(projectApiKey)}&projectId=${encodeURIComponent(selectedProjectId)}`, ); es.onopen = () => { diff --git a/dashboard/src/components/Sidebar.tsx b/dashboard/src/components/Sidebar.tsx index 1397f8e..300f1a3 100644 --- a/dashboard/src/components/Sidebar.tsx +++ b/dashboard/src/components/Sidebar.tsx @@ -14,8 +14,11 @@ import { Clock, GitFork, FileCode2, + LogOut, + UserCheck, } from "lucide-react"; import { cn } from "@/lib/utils"; +import { useAuth } from "@/hooks/useAuth"; const navigation = [ { name: "Live Feed", href: "/", icon: Activity }, @@ -32,6 +35,16 @@ const navigation = [ export default function Sidebar() { const pathname = usePathname(); + const { user, logout, isAuthenticated } = useAuth(); + + const isLoginPage = + pathname === "/login" || + pathname === "/login/" || + pathname?.endsWith("/login") || + pathname?.endsWith("/login/"); + if (isLoginPage || !isAuthenticated) { + return null; + } return (
@@ -39,12 +52,17 @@ export default function Sidebar() {
-

- Notifkit -

+
+

+ Notifkit +

+ + Admin Console + +
-