87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px] whitespace-nowrap",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
24 | ))
25 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26 |
27 | const toastVariants = cva(
28 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
29 | {
30 | variants: {
31 | variant: {
32 | default: "border bg-background text-foreground",
33 | destructive:
34 | "destructive group border-destructive bg-destructive text-destructive-foreground",
35 | },
36 | },
37 | defaultVariants: {
38 | variant: "default",
39 | },
40 | }
41 | )
42 |
43 | const Toast = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef &
46 | VariantProps
47 | >(({ className, variant, ...props }, ref) => {
48 | return (
49 |
54 | )
55 | })
56 | Toast.displayName = ToastPrimitives.Root.displayName
57 |
58 | const ToastAction = React.forwardRef<
59 | React.ElementRef,
60 | React.ComponentPropsWithoutRef
61 | >(({ className, ...props }, ref) => (
62 |
70 | ))
71 | ToastAction.displayName = ToastPrimitives.Action.displayName
72 |
73 | const ToastClose = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
86 |
87 |
88 | ))
89 | ToastClose.displayName = ToastPrimitives.Close.displayName
90 |
91 | const ToastTitle = React.forwardRef<
92 | React.ElementRef,
93 | React.ComponentPropsWithoutRef
94 | >(({ className, ...props }, ref) => (
95 |
100 | ))
101 | ToastTitle.displayName = ToastPrimitives.Title.displayName
102 |
103 | const ToastDescription = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, ...props }, ref) => (
107 |
112 | ))
113 | ToastDescription.displayName = ToastPrimitives.Description.displayName
114 |
115 | type ToastProps = React.ComponentPropsWithoutRef
116 |
117 | type ToastActionElement = React.ReactElement
118 |
119 | export {
120 | type ToastProps,
121 | type ToastActionElement,
122 | ToastProvider,
123 | ToastViewport,
124 | Toast,
125 | ToastTitle,
126 | ToastDescription,
127 | ToastClose,
128 | ToastAction,
129 | }
130 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useToast } from "@/hooks/use-toast"
4 | import {
5 | Toast,
6 | ToastClose,
7 | ToastDescription,
8 | ToastProvider,
9 | ToastTitle,
10 | ToastViewport,
11 | } from "@/components/ui/toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | rabbitscout:
3 | build: .
4 | ports:
5 | - "3000:3000"
6 | environment:
7 | - NEXT_PUBLIC_RABBITMQ_HOST
8 | - NEXT_PUBLIC_RABBITMQ_PORT
9 | - NEXT_PUBLIC_RABBITMQ_VHOST
10 | - RABBITMQ_USERNAME
11 | - RABBITMQ_PASSWORD
12 | - NEXT_PUBLIC_API_URL
13 |
--------------------------------------------------------------------------------
/docs/assets/dark-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/docs/assets/dark-dashboard.png
--------------------------------------------------------------------------------
/docs/assets/light-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/docs/assets/light-dashboard.png
--------------------------------------------------------------------------------
/docs/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/docs/assets/logo.png
--------------------------------------------------------------------------------
/hooks/use-realtime-updates.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 |
3 | export type UpdateType = "queue" | "exchange" | "connection" | "channel"
4 |
5 | export function useRealtimeUpdates(
6 | type: UpdateType,
7 | initialData: T[],
8 | updateHandler?: (current: T[], update: any) => T[]
9 | ): T[] {
10 | const [data, setData] = useState(initialData)
11 |
12 | // Only update data if initialData reference changes
13 | useEffect(() => {
14 | setData(initialData)
15 | }, [initialData])
16 |
17 | // WebSocket functionality temporarily disabled
18 | /*useEffect(() => {
19 | const socket = new WebSocket(`ws://${RABBITMQ_CONFIG.host}:15674/ws`)
20 |
21 | socket.onopen = () => {
22 | console.log(`[Realtime] WebSocket connected for ${type}`);
23 | socket.send(JSON.stringify({ command: 'subscribe', type }))
24 | }
25 |
26 | socket.onmessage = (event) => {
27 | const update = JSON.parse(event.data)
28 | console.log(`[Realtime] Received ${type} update:`, update);
29 | setData(current => updateHandler!(current, update))
30 | }
31 |
32 | socket.onerror = (error) => {
33 | console.error(`[Realtime] WebSocket error for ${type}:`, error)
34 | }
35 |
36 | socket.onclose = () => {
37 | console.log(`[Realtime] WebSocket closed for ${type}`)
38 | }
39 |
40 | return () => {
41 | socket.close()
42 | }
43 | }, [type, updateHandler])*/
44 |
45 | return data
46 | }
47 |
48 | // Example usage:
49 | // const queues = useRealtimeUpdates('queue', initialQueues, (current, update) => {
50 | // return current.map(queue =>
51 | // queue.name === update.name ? { ...queue, ...update } : queue
52 | // )
53 | // })
54 |
--------------------------------------------------------------------------------
/hooks/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/lib/api-utils.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 |
3 | // Common cache prevention headers
4 | export const NO_CACHE_HEADERS = {
5 | 'Cache-Control': 'no-cache, no-store, must-revalidate',
6 | 'Pragma': 'no-cache',
7 | 'Expires': '0'
8 | }
9 |
10 | // Common fetch options to prevent caching
11 | export const NO_CACHE_FETCH_OPTIONS = {
12 | cache: 'no-store' as RequestCache
13 | }
14 |
15 | // Helper function to create a response with no-cache headers
16 | export function createApiResponse(data: any, options: { status?: number } = {}) {
17 | return NextResponse.json(data, {
18 | status: options.status || 200,
19 | headers: NO_CACHE_HEADERS
20 | })
21 | }
22 |
23 | // Helper function to create an error response with no-cache headers
24 | export function createApiErrorResponse(error: string, status: number = 500) {
25 | return NextResponse.json(
26 | { error },
27 | {
28 | status,
29 | headers: NO_CACHE_HEADERS
30 | }
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { persist, createJSONStorage } from 'zustand/middleware';
3 | import Cookies from 'js-cookie';
4 |
5 | interface User {
6 | username: string;
7 | isAdmin: boolean;
8 | tags: string[];
9 | }
10 |
11 | interface AuthState {
12 | authenticated: boolean;
13 | user: User | null;
14 | login: (authData: { authenticated: boolean; user: User }) => void;
15 | logout: () => void;
16 | }
17 |
18 | const isBrowser = typeof window !== 'undefined';
19 |
20 | // Safe storage that works in both client and server environments
21 | const safeStorage = {
22 | getItem: (name: string): string | null => {
23 | if (!isBrowser) return null;
24 | try {
25 | return localStorage.getItem(name);
26 | } catch (error) {
27 | console.error('Error getting storage item:', error);
28 | return null;
29 | }
30 | },
31 | setItem: (name: string, value: string) => {
32 | if (!isBrowser) return;
33 | try {
34 | localStorage.setItem(name, value);
35 | // Also set in cookies for SSR
36 | Cookies.set('auth-storage', value, { path: '/' });
37 | } catch (error) {
38 | console.error('Error setting storage item:', error);
39 | }
40 | },
41 | removeItem: (name: string) => {
42 | if (!isBrowser) return;
43 | try {
44 | localStorage.removeItem(name);
45 | // Also remove from cookies
46 | Cookies.remove('auth-storage', { path: '/' });
47 | } catch (error) {
48 | console.error('Error removing storage item:', error);
49 | }
50 | }
51 | };
52 |
53 | export const useAuth = create()(
54 | persist(
55 | (set) => ({
56 | authenticated: false,
57 | user: null,
58 | login: (authData) => set({ authenticated: authData.authenticated, user: authData.user }),
59 | logout: () => set({ authenticated: false, user: null }),
60 | }),
61 | {
62 | name: 'auth-storage',
63 | storage: createJSONStorage(() => safeStorage),
64 | skipHydration: true, // Important for SSR
65 | }
66 | )
67 | );
68 |
--------------------------------------------------------------------------------
/lib/config.ts:
--------------------------------------------------------------------------------
1 | import { useAuth } from './auth'
2 |
3 | export const RABBITMQ_CONFIG = {
4 | host: process.env.NEXT_PUBLIC_RABBITMQ_HOST,
5 | port: process.env.NEXT_PUBLIC_RABBITMQ_PORT,
6 | vhost: process.env.NEXT_PUBLIC_RABBITMQ_VHOST ,
7 | username: process.env.RABBITMQ_USERNAME,
8 | password: process.env.RABBITMQ_PASSWORD,
9 | }
10 |
11 | export const API_TIMEOUT_MS = process.env.NEXT_PUBLIC_RABBITMQ_API_TIMEOUT_MS
12 | ? parseInt(process.env.NEXT_PUBLIC_RABBITMQ_API_TIMEOUT_MS, 10)
13 | : 60000; // Default to 60 seconds (60000 ms)
14 |
15 | export const getRabbitMQConfig = () => {
16 | const auth = useAuth.getState()
17 |
18 | return {
19 | host: RABBITMQ_CONFIG.host,
20 | port: RABBITMQ_CONFIG.port,
21 | vhost: RABBITMQ_CONFIG.vhost,
22 | username: auth.user?.username || RABBITMQ_CONFIG.username,
23 | password: RABBITMQ_CONFIG.password,
24 | }
25 | }
26 |
27 | // Helper to get base URL
28 | export const getRabbitMQBaseUrl = () => {
29 | return `http://${RABBITMQ_CONFIG.host}:${RABBITMQ_CONFIG.port}`
30 | }
31 |
32 | // Helper to get auth headers using current auth state
33 | // This function is now simplified for server-side use only, for direct calls
34 | // from Server Components to RabbitMQ. Client-side calls use the /api proxy.
35 | export const getRabbitMQAuthHeaders = () => {
36 | // Always use the server-side configured credentials from RABBITMQ_CONFIG in this file
37 | const credentials = Buffer.from(`${RABBITMQ_CONFIG.username}:${RABBITMQ_CONFIG.password}`).toString('base64');
38 | return {
39 | 'Authorization': `Basic ${credentials}`,
40 | 'Content-Type': 'application/json',
41 | };
42 | }
43 |
--------------------------------------------------------------------------------
/lib/hooks/use-graph-data.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | interface GraphData {
4 | messageRateData: Array<{
5 | timestamp: number;
6 | publishRate: number;
7 | deliveryRate: number;
8 | }>;
9 | queuedMessagesData: Array<{
10 | timestamp: number;
11 | messages: number;
12 | messagesReady: number;
13 | messagesUnacked: number;
14 | }>;
15 | }
16 |
17 | export function useGraphData(): GraphData {
18 | const [messageRateData, setMessageRateData] = useState([]);
19 | const [queuedMessagesData, setQueuedMessagesData] = useState([]);
20 |
21 | useEffect(() => {
22 | const updateGraphData = async () => {
23 | try {
24 | const response = await fetch('/api/overview');
25 | const data = await response.json();
26 |
27 | const timestamp = Date.now();
28 | const oneMinuteAgo = timestamp - 60 * 1000;
29 |
30 | // Update message rate data
31 | setMessageRateData(prev => {
32 | const newPoint = {
33 | timestamp,
34 | publishRate: data.message_stats?.publish_details?.rate || 0,
35 | deliveryRate: data.message_stats?.deliver_get_details?.rate || 0,
36 | };
37 | const filtered = prev.filter(point => point.timestamp > oneMinuteAgo);
38 | return [...filtered, newPoint];
39 | });
40 |
41 | // Update queued messages data
42 | setQueuedMessagesData(prev => {
43 | const newPoint = {
44 | timestamp,
45 | messages: data.queue_totals?.messages || 0,
46 | messagesReady: data.queue_totals?.messages_ready || 0,
47 | messagesUnacked: data.queue_totals?.messages_unacknowledged || 0,
48 | };
49 | const filtered = prev.filter(point => point.timestamp > oneMinuteAgo);
50 | return [...filtered, newPoint];
51 | });
52 | } catch (error) {
53 | console.error('Error fetching graph data:', error);
54 | }
55 | };
56 |
57 | // Initial update
58 | updateGraphData();
59 |
60 | // Set up 5-second interval
61 | const interval = setInterval(updateGraphData, 5000);
62 |
63 | return () => clearInterval(interval);
64 | }, []);
65 |
66 | return {
67 | messageRateData,
68 | queuedMessagesData,
69 | };
70 | }
71 |
--------------------------------------------------------------------------------
/lib/store.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { persist } from 'zustand/middleware'
3 |
4 | interface RefreshState {
5 | interval: number
6 | setInterval: (interval: number) => void
7 | }
8 |
9 | export const useRefreshStore = create()(
10 | persist(
11 | (set) => ({
12 | interval: 5, // Default 5 seconds
13 | setInterval: (interval) => set({ interval }),
14 | }),
15 | {
16 | name: 'refresh-settings',
17 | }
18 | )
19 | )
20 |
--------------------------------------------------------------------------------
/lib/websocket.ts:
--------------------------------------------------------------------------------
1 | import { RABBITMQ_CONFIG } from "@/lib/utils"
2 |
3 | type MessageHandler = (data: any) => void
4 |
5 | class RabbitMQWebSocket {
6 | private ws: WebSocket | null = null
7 | private reconnectAttempts = 0
8 | private maxReconnectAttempts = 5
9 | private reconnectDelay = 1000
10 | private handlers: Set = new Set()
11 |
12 | constructor() {
13 | if (typeof window !== "undefined") {
14 | this.connect()
15 | }
16 | }
17 |
18 | private connect() {
19 | const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"
20 | const credentials = btoa(
21 | `${RABBITMQ_CONFIG.username}:${RABBITMQ_CONFIG.password}`
22 | )
23 |
24 | this.ws = new WebSocket(
25 | `${protocol}//${RABBITMQ_CONFIG.host}:${RABBITMQ_CONFIG.port}/ws`
26 | )
27 |
28 | this.ws.onopen = () => {
29 | console.log("WebSocket connected")
30 | this.reconnectAttempts = 0
31 | this.ws?.send(
32 | JSON.stringify({
33 | type: "auth",
34 | credentials,
35 | })
36 | )
37 | }
38 |
39 | this.ws.onmessage = (event) => {
40 | try {
41 | const data = JSON.parse(event.data)
42 | this.handlers.forEach((handler) => handler(data))
43 | } catch (error) {
44 | console.error("Failed to parse WebSocket message:", error)
45 | }
46 | }
47 |
48 | this.ws.onclose = () => {
49 | console.log("WebSocket disconnected")
50 | if (this.reconnectAttempts < this.maxReconnectAttempts) {
51 | setTimeout(() => {
52 | this.reconnectAttempts++
53 | this.connect()
54 | }, this.reconnectDelay * Math.pow(2, this.reconnectAttempts))
55 | }
56 | }
57 |
58 | this.ws.onerror = (error) => {
59 | console.error("WebSocket error:", error)
60 | }
61 | }
62 |
63 | subscribe(handler: MessageHandler) {
64 | this.handlers.add(handler)
65 | return () => this.handlers.delete(handler)
66 | }
67 |
68 | send(data: any) {
69 | if (this.ws?.readyState === WebSocket.OPEN) {
70 | this.ws.send(JSON.stringify(data))
71 | }
72 | }
73 | }
74 |
75 | export const rabbitMQWebSocket = new RabbitMQWebSocket()
76 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import type { NextRequest } from 'next/server'
3 |
4 | export function middleware(request: NextRequest) {
5 | const authCookie = request.cookies.get('auth-storage')
6 |
7 | let isAuthenticated = false
8 | let userData = null
9 |
10 | try {
11 | if (authCookie?.value) {
12 | let cookieValue = authCookie.value
13 | try {
14 | cookieValue = decodeURIComponent(cookieValue)
15 | } catch (e) {
16 | // If decoding fails, use the raw value
17 | }
18 |
19 | const parsed = JSON.parse(cookieValue)
20 | isAuthenticated = parsed.state?.authenticated || false
21 | userData = parsed.state?.user
22 | }
23 | } catch (error) {
24 | console.error('Error parsing auth cookie:', error)
25 | }
26 |
27 | // Only log auth state for non-API routes to reduce noise
28 | if (!request.nextUrl.pathname.startsWith('/api')) {
29 | console.log('Auth state:', { isAuthenticated, path: request.nextUrl.pathname })
30 | }
31 |
32 | // Skip auth check for public API routes
33 | if (request.nextUrl.pathname.startsWith('/api/auth')) {
34 | return NextResponse.next()
35 | }
36 |
37 | // Protect all routes except login
38 | if (!isAuthenticated && !request.nextUrl.pathname.startsWith('/login')) {
39 | const loginUrl = new URL('/login', request.url)
40 | console.log('Redirecting to:', loginUrl.toString())
41 | return NextResponse.redirect(loginUrl)
42 | }
43 |
44 | // Redirect authenticated users away from login page
45 | if (isAuthenticated && request.nextUrl.pathname.startsWith('/login')) {
46 | const homeUrl = new URL('/', request.url)
47 | console.log('Redirecting to:', homeUrl.toString())
48 | return NextResponse.redirect(homeUrl)
49 | }
50 |
51 | return NextResponse.next()
52 | }
53 |
54 | export const config = {
55 | matcher: [
56 | '/((?!_next/static|_next/image|favicon.ico).*)',
57 | ],
58 | }
59 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | eslint: {
4 | ignoreDuringBuilds: true,
5 | },
6 | typescript: {
7 | ignoreBuildErrors: true,
8 | },
9 | output: 'standalone',
10 | images: {
11 | unoptimized: true,
12 | },
13 | };
14 |
15 | module.exports = nextConfig;
16 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rabbitscout",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@radix-ui/react-dialog": "^1.1.2",
13 | "@radix-ui/react-dropdown-menu": "^2.1.2",
14 | "@radix-ui/react-label": "^2.1.0",
15 | "@radix-ui/react-select": "^2.1.2",
16 | "@radix-ui/react-slot": "^1.1.0",
17 | "@radix-ui/react-toast": "^1.2.2",
18 | "@types/js-cookie": "^3.0.6",
19 | "class-variance-authority": "^0.7.1",
20 | "clsx": "^2.1.1",
21 | "date-fns": "^4.1.0",
22 | "js-cookie": "^3.0.5",
23 | "lucide-react": "^0.462.0",
24 | "next": "14.2.16",
25 | "next-themes": "^0.4.3",
26 | "react": "^18",
27 | "react-dom": "^18",
28 | "recharts": "^2.13.3",
29 | "tailwind-merge": "^2.5.5",
30 | "tailwindcss-animate": "^1.0.7",
31 | "zustand": "^5.0.1"
32 | },
33 | "devDependencies": {
34 | "@types/node": "^20",
35 | "@types/react": "^18",
36 | "@types/react-dom": "^18",
37 | "eslint": "^8",
38 | "eslint-config-next": "14.2.16",
39 | "postcss": "^8",
40 | "tailwindcss": "^3.4.1",
41 | "typescript": "^5"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Ralve-org/RabbitScout/aaf2b312215616a0a41b1f15ff8734be80cad7c7/public/images/logo.png
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | darkMode: ["class"],
5 | content: [
6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | background: 'hsl(var(--background))',
14 | foreground: 'hsl(var(--foreground))',
15 | card: {
16 | DEFAULT: 'hsl(var(--card))',
17 | foreground: 'hsl(var(--card-foreground))'
18 | },
19 | popover: {
20 | DEFAULT: 'hsl(var(--popover))',
21 | foreground: 'hsl(var(--popover-foreground))'
22 | },
23 | primary: {
24 | DEFAULT: 'hsl(var(--primary))',
25 | foreground: 'hsl(var(--primary-foreground))'
26 | },
27 | secondary: {
28 | DEFAULT: 'hsl(var(--secondary))',
29 | foreground: 'hsl(var(--secondary-foreground))'
30 | },
31 | muted: {
32 | DEFAULT: 'hsl(var(--muted))',
33 | foreground: 'hsl(var(--muted-foreground))'
34 | },
35 | accent: {
36 | DEFAULT: 'hsl(var(--accent))',
37 | foreground: 'hsl(var(--accent-foreground))'
38 | },
39 | destructive: {
40 | DEFAULT: 'hsl(var(--destructive))',
41 | foreground: 'hsl(var(--destructive-foreground))'
42 | },
43 | border: 'hsl(var(--border))',
44 | input: 'hsl(var(--input))',
45 | ring: 'hsl(var(--ring))',
46 | chart: {
47 | '1': 'hsl(var(--chart-1))',
48 | '2': 'hsl(var(--chart-2))',
49 | '3': 'hsl(var(--chart-3))',
50 | '4': 'hsl(var(--chart-4))',
51 | '5': 'hsl(var(--chart-5))'
52 | }
53 | },
54 | borderRadius: {
55 | lg: 'var(--radius)',
56 | md: 'calc(var(--radius) - 2px)',
57 | sm: 'calc(var(--radius) - 4px)'
58 | }
59 | }
60 | },
61 | plugins: [require("tailwindcss-animate")],
62 | };
63 | export default config;
64 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
|