├── .env.example ├── .gitignore ├── Dockerfile ├── README.md ├── app ├── .DS_Store ├── animation-demo │ └── page.tsx ├── api │ ├── .DS_Store │ ├── analytics │ │ └── performance │ │ │ └── route.ts │ ├── auth │ │ ├── change-password │ │ │ └── route.ts │ │ ├── check-users │ │ │ └── route.ts │ │ ├── get-user │ │ │ └── route.ts │ │ ├── login │ │ │ └── route.ts │ │ ├── logout │ │ │ └── route.ts │ │ └── register-first-user │ │ │ └── route.ts │ ├── browser-analysis │ │ └── route.ts │ ├── debug-large-zip │ │ └── route.ts │ ├── device-credentials │ │ └── route.ts │ ├── download-device │ │ └── route.ts │ ├── file-content │ │ └── route.ts │ ├── rss-feeds │ │ └── route.ts │ ├── search │ │ └── route.ts │ ├── software-analysis │ │ └── route.ts │ ├── stats │ │ └── route.ts │ ├── top-tlds │ │ └── route.ts │ ├── upload-logs │ │ └── route.ts │ └── upload │ │ └── route.ts ├── components │ └── logout-button.tsx ├── dashboard │ ├── loading.tsx │ └── page.tsx ├── debug-zip │ └── page.tsx ├── globals.css ├── layout.tsx ├── loading.tsx ├── login │ └── page.tsx ├── page.tsx └── upload │ └── page.tsx ├── components.json ├── components ├── animated-counter.tsx ├── animated-software-list.tsx ├── animated-stat-card.tsx ├── animation-demo.tsx ├── app-header.tsx ├── app-sidebar.tsx ├── auth-guard.tsx ├── browser-vertical-bar-chart.tsx ├── change-password-modal.tsx ├── client-layout-with-sidebar.tsx ├── device │ ├── CredentialsTable.tsx │ └── DeviceDetailsPanel.tsx ├── error-boundary.tsx ├── file │ ├── FileContentDialog.tsx │ └── FileTreeViewer.tsx ├── force-refresh-wrapper.tsx ├── search │ ├── SearchInterface.tsx │ ├── SearchResults.tsx │ └── TypingEffect.tsx ├── theme-provider.tsx ├── ui │ ├── accordion.tsx │ ├── alert-dialog.tsx │ ├── alert.tsx │ ├── aspect-ratio.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── card.tsx │ ├── carousel.tsx │ ├── chart.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── context-menu.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── form.tsx │ ├── hover-card.tsx │ ├── input-otp.tsx │ ├── input.tsx │ ├── label.tsx │ ├── loading.tsx │ ├── menubar.tsx │ ├── navigation-menu.tsx │ ├── pagination.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── radio-group.tsx │ ├── resizable.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── sheet.tsx │ ├── sidebar.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── sonner.tsx │ ├── switch.tsx │ ├── table.tsx │ ├── tabs.tsx │ ├── textarea.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle-group.tsx │ ├── toggle.tsx │ ├── tooltip.tsx │ ├── use-mobile.tsx │ └── use-toast.ts └── user-profile-dropdown.tsx ├── docker-compose.yml ├── hooks ├── use-mobile.tsx ├── use-toast.ts ├── useAuth.ts ├── useSearch.ts └── useStats.ts ├── images ├── Bron-Vault---Search-I.png ├── Bron-Vault---Search-II.png ├── Bron-Vault---Search-III.png └── Bron-Vault-Dashboard.jpeg ├── lib ├── accessibility.ts ├── auth.ts ├── file-tree-utils.tsx ├── logger.ts ├── memory-storage.ts ├── mysql.ts ├── performance.ts ├── rate-limiter.ts ├── software-parser.ts ├── upload-connections.ts ├── utils.ts └── validation.ts ├── middleware.ts ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── postcss.config.mjs ├── public ├── .DS_Store ├── images │ ├── favicon.png │ ├── logo-light.png │ └── logo.png ├── placeholder-logo.png ├── placeholder-logo.svg ├── placeholder-user.jpg ├── placeholder.jpg └── placeholder.svg ├── scripts ├── 001_create_tables.sql ├── 002_create_users_table.sql ├── 004_add_local_file_path.sql ├── analyze-bundle.js └── mysql-setup.sql ├── styles └── globals.css ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # Add MYSQL_ROOT_PASSWORD and remove MYSQL_HOST if you're using docker 2 | MYSQL_HOST=localhost 3 | MYSQL_PORT=3306 4 | MYSQL_USER=dbuser 5 | MYSQL_PASSWORD=dbpass 6 | MYSQL_DATABASE=dbname -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .env.local 3 | node_modules/* 4 | .next/* 5 | uploads/* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:20-alpine AS deps 3 | WORKDIR /app 4 | COPY package.json ./ 5 | RUN yarn install --frozen-lockfile 6 | 7 | # Rebuild the source code only when needed 8 | FROM node:20-alpine AS builder 9 | WORKDIR /app 10 | COPY . . 11 | COPY --from=deps /app/node_modules ./node_modules 12 | RUN yarn build 13 | 14 | # Production image, copy all the files and run next 15 | FROM node:20-alpine AS runner 16 | WORKDIR /app 17 | ENV NODE_ENV=production 18 | 19 | # Copy only necessary files 20 | COPY --from=builder /app/.next ./.next 21 | COPY --from=builder /app/public ./public 22 | COPY --from=builder /app/package.json ./package.json 23 | COPY --from=builder /app/node_modules ./node_modules 24 | COPY --from=builder /app/next.config.mjs ./next.config.mjs 25 | COPY --from=builder /app/tailwind.config.ts ./tailwind.config.ts 26 | COPY --from=builder /app/postcss.config.mjs ./postcss.config.mjs 27 | COPY --from=builder /app/app ./app 28 | COPY --from=builder /app/components ./components 29 | COPY --from=builder /app/hooks ./hooks 30 | COPY --from=builder /app/lib ./lib 31 | COPY --from=builder /app/styles ./styles 32 | 33 | EXPOSE 3000 34 | 35 | CMD ["yarn", "start"] -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITSEC-Research/bron-vault/77dabb1534e5232c16e6ddb0345cb663c170facc/app/.DS_Store -------------------------------------------------------------------------------- /app/animation-demo/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { AnimationDemo } from "@/components/animation-demo" 4 | 5 | export default function AnimationDemoPage() { 6 | return ( 7 |
8 |
9 |
10 |

11 | 🎬 Dashboard Animation Demo 12 |

13 |

14 | Animation demonstration that has been added to the broń Vault dashboard 15 |

16 |
17 | 18 | 19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ITSEC-Research/bron-vault/77dabb1534e5232c16e6ddb0345cb663c170facc/app/api/.DS_Store -------------------------------------------------------------------------------- /app/api/analytics/performance/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from 'next/server' 2 | import { logInfo, logWarn } from '@/lib/logger' 3 | import { validateRequest } from '@/lib/auth' 4 | 5 | interface PerformanceMetric { 6 | metric: string 7 | value: number 8 | rating: 'good' | 'needs-improvement' | 'poor' 9 | timestamp: number 10 | url: string 11 | userAgent: string 12 | } 13 | 14 | // In-memory storage for demo purposes 15 | // In production, you'd want to use a proper database or analytics service 16 | const performanceMetrics: PerformanceMetric[] = [] 17 | 18 | export async function POST(request: NextRequest) { 19 | // Validate authentication 20 | const user = await validateRequest(request) 21 | if (!user) { 22 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 23 | } 24 | 25 | try { 26 | const metric: PerformanceMetric = await request.json() 27 | 28 | // Validate the metric data 29 | if (!metric.metric || typeof metric.value !== 'number') { 30 | return NextResponse.json( 31 | { error: 'Invalid metric data' }, 32 | { status: 400 } 33 | ) 34 | } 35 | 36 | // Store the metric 37 | performanceMetrics.push(metric) 38 | 39 | // Log poor performance metrics 40 | if (metric.rating === 'poor') { 41 | logWarn( 42 | `Poor performance detected: ${metric.metric} = ${metric.value}ms`, 43 | metric, 44 | 'Performance Analytics' 45 | ) 46 | } else { 47 | logInfo( 48 | `Performance metric recorded: ${metric.metric} = ${metric.value}ms`, 49 | undefined, 50 | 'Performance Analytics' 51 | ) 52 | } 53 | 54 | // Keep only last 1000 metrics to prevent memory issues 55 | if (performanceMetrics.length > 1000) { 56 | performanceMetrics.splice(0, performanceMetrics.length - 1000) 57 | } 58 | 59 | return NextResponse.json({ success: true }) 60 | } catch (error) { 61 | logWarn('Failed to process performance metric', error, 'Performance Analytics') 62 | return NextResponse.json( 63 | { error: 'Failed to process metric' }, 64 | { status: 500 } 65 | ) 66 | } 67 | } 68 | 69 | export async function GET(request: NextRequest) { 70 | // Validate authentication 71 | const user = await validateRequest(request) 72 | if (!user) { 73 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 74 | } 75 | 76 | try { 77 | const { searchParams } = new URL(request.url) 78 | const metric = searchParams.get('metric') 79 | const limit = parseInt(searchParams.get('limit') || '100') 80 | 81 | let filteredMetrics = performanceMetrics 82 | 83 | // Filter by metric type if specified 84 | if (metric) { 85 | filteredMetrics = performanceMetrics.filter(m => m.metric === metric) 86 | } 87 | 88 | // Limit results 89 | const results = filteredMetrics 90 | .slice(-limit) 91 | .sort((a, b) => b.timestamp - a.timestamp) 92 | 93 | // Calculate summary statistics 94 | const summary = calculateSummary(filteredMetrics) 95 | 96 | return NextResponse.json({ 97 | metrics: results, 98 | summary, 99 | total: filteredMetrics.length 100 | }) 101 | } catch (error) { 102 | logWarn('Failed to retrieve performance metrics', error, 'Performance Analytics') 103 | return NextResponse.json( 104 | { error: 'Failed to retrieve metrics' }, 105 | { status: 500 } 106 | ) 107 | } 108 | } 109 | 110 | function calculateSummary(metrics: PerformanceMetric[]) { 111 | if (metrics.length === 0) { 112 | return { 113 | count: 0, 114 | averageValue: 0, 115 | ratings: { good: 0, needsImprovement: 0, poor: 0 } 116 | } 117 | } 118 | 119 | const totalValue = metrics.reduce((sum, m) => sum + m.value, 0) 120 | const averageValue = totalValue / metrics.length 121 | 122 | const ratings = metrics.reduce( 123 | (acc, m) => { 124 | acc[m.rating === 'needs-improvement' ? 'needsImprovement' : m.rating]++ 125 | return acc 126 | }, 127 | { good: 0, needsImprovement: 0, poor: 0 } 128 | ) 129 | 130 | return { 131 | count: metrics.length, 132 | averageValue: Math.round(averageValue * 100) / 100, 133 | ratings 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /app/api/auth/change-password/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { pool } from "@/lib/mysql"; 3 | import bcrypt from "bcryptjs"; 4 | import type { RowDataPacket } from "mysql2"; 5 | import { validateRequest } from "@/lib/auth"; 6 | 7 | export async function POST(request: NextRequest) { 8 | const { currentPassword, newPassword } = await request.json(); 9 | 10 | // Use JWT validation instead of direct cookie access 11 | const user = await validateRequest(request); 12 | 13 | if (!user) { 14 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); 15 | } 16 | 17 | try { 18 | // Get current user data 19 | const [users] = await pool.query( 20 | "SELECT id, password_hash FROM users WHERE id = ? LIMIT 1", 21 | [user.userId] 22 | ); 23 | 24 | if (!Array.isArray(users) || users.length === 0) { 25 | return NextResponse.json({ success: false, error: "User not found" }, { status: 404 }); 26 | } 27 | 28 | const userData = users[0]; 29 | 30 | // Verify current password 31 | const isCurrentPasswordValid = await bcrypt.compare(currentPassword, userData.password_hash); 32 | if (!isCurrentPasswordValid) { 33 | return NextResponse.json({ success: false, error: "Current password is incorrect" }, { status: 400 }); 34 | } 35 | 36 | // Hash new password 37 | const newPasswordHash = await bcrypt.hash(newPassword, 12); 38 | 39 | // Update password 40 | await pool.query( 41 | "UPDATE users SET password_hash = ? WHERE id = ?", 42 | [newPasswordHash, user.userId] 43 | ); 44 | 45 | return NextResponse.json({ success: true, message: "Password updated successfully" }); 46 | } catch (err) { 47 | console.error("Change password error:", err); 48 | return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); 49 | } 50 | } -------------------------------------------------------------------------------- /app/api/auth/check-users/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { pool } from "@/lib/mysql" 3 | import type { RowDataPacket } from "mysql2" 4 | 5 | // Force dynamic rendering 6 | export const dynamic = 'force-dynamic' 7 | 8 | export async function GET(request: NextRequest) { 9 | console.log("🔍 [PRODUCTION] Check users endpoint called") 10 | try { 11 | // Check if users table exists 12 | const [tables] = await pool.query( 13 | "SHOW TABLES LIKE 'users'" 14 | ) 15 | 16 | if (!Array.isArray(tables) || tables.length === 0) { 17 | // Users table doesn't exist, return 0 users 18 | return NextResponse.json({ 19 | success: true, 20 | userCount: 0, 21 | needsInitialSetup: true 22 | }) 23 | } 24 | 25 | // Count total users 26 | const [result] = await pool.query( 27 | "SELECT COUNT(*) as count FROM users" 28 | ) 29 | 30 | const userCount = Array.isArray(result) && result.length > 0 ? result[0].count : 0 31 | 32 | return NextResponse.json({ 33 | success: true, 34 | userCount: userCount, 35 | needsInitialSetup: userCount === 0 36 | }) 37 | } catch (err) { 38 | console.error("Check users error:", err) 39 | return NextResponse.json({ 40 | success: false, 41 | error: "Failed to check users", 42 | userCount: 0, 43 | needsInitialSetup: true 44 | }, { status: 500 }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/api/auth/get-user/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { pool } from "@/lib/mysql"; 3 | import type { RowDataPacket } from "mysql2"; 4 | import { validateRequest } from "@/lib/auth"; 5 | 6 | export async function GET(request: NextRequest) { 7 | // Use JWT validation instead of direct cookie access 8 | const user = await validateRequest(request); 9 | 10 | if (!user) { 11 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }); 12 | } 13 | 14 | try { 15 | const [users] = await pool.query( 16 | "SELECT id, email, name FROM users WHERE id = ? LIMIT 1", 17 | [user.userId] 18 | ); 19 | 20 | if (!Array.isArray(users) || users.length === 0) { 21 | return NextResponse.json({ success: false, error: "User not found" }, { status: 404 }); 22 | } 23 | 24 | const userData = users[0]; 25 | return NextResponse.json({ 26 | success: true, 27 | user: { 28 | id: userData.id, 29 | email: userData.email, 30 | name: userData.name || userData.email.split('@')[0] // Fallback to email prefix if name is null 31 | } 32 | }); 33 | } catch (err) { 34 | console.error("Get user error:", err); 35 | return NextResponse.json({ success: false, error: "Internal server error" }, { status: 500 }); 36 | } 37 | } -------------------------------------------------------------------------------- /app/api/auth/login/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { pool } from "@/lib/mysql" 3 | import bcrypt from "bcryptjs" 4 | import type { RowDataPacket } from "mysql2" 5 | import { generateToken, getSecureCookieOptions } from "@/lib/auth" 6 | 7 | export async function POST(request: NextRequest) { 8 | const { email, password } = await request.json() 9 | 10 | try { 11 | const [users] = await pool.query( 12 | "SELECT id, email, password_hash, name FROM users WHERE email = ? LIMIT 1", 13 | [email] 14 | ) 15 | 16 | if (!Array.isArray(users) || users.length === 0) { 17 | return NextResponse.json({ success: false, error: "Invalid email or password." }, { status: 401 }) 18 | } 19 | 20 | const user = users[0] 21 | 22 | // Verify password hash 23 | const match = await bcrypt.compare(password, user.password_hash || "") 24 | if (!match) { 25 | return NextResponse.json({ success: false, error: "Invalid email or password." }, { status: 401 }) 26 | } 27 | 28 | // Generate JWT token 29 | const token = await generateToken({ 30 | userId: String(user.id), 31 | username: user.name || user.email, 32 | }) 33 | 34 | // Set secure cookie with JWT token 35 | const response = NextResponse.json({ 36 | success: true, 37 | user: { 38 | id: user.id, 39 | email: user.email, 40 | name: user.name 41 | } 42 | }) 43 | 44 | response.cookies.set("auth", token, getSecureCookieOptions()) 45 | 46 | return response 47 | } catch (err) { 48 | console.error("Login error:", err) 49 | return NextResponse.json({ success: false, error: "Internal server error occurred." }, { status: 500 }) 50 | } 51 | } -------------------------------------------------------------------------------- /app/api/auth/logout/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { getSecureCookieOptions } from "@/lib/auth"; 3 | 4 | export async function POST(request: NextRequest) { 5 | const response = NextResponse.json({ success: true }); 6 | 7 | // Clear the auth cookie with consistent secure options 8 | response.cookies.set("auth", "", { 9 | ...getSecureCookieOptions(), 10 | maxAge: 0, // Override maxAge to 0 to clear the cookie 11 | }); 12 | 13 | return response; 14 | } -------------------------------------------------------------------------------- /app/api/auth/register-first-user/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { pool } from "@/lib/mysql" 3 | import bcrypt from "bcryptjs" 4 | import type { RowDataPacket } from "mysql2" 5 | 6 | // Force dynamic rendering 7 | export const dynamic = 'force-dynamic' 8 | 9 | export async function POST(request: NextRequest) { 10 | try { 11 | const { email, password, name } = await request.json() 12 | 13 | // Validate input 14 | if (!email || !password || !name) { 15 | return NextResponse.json({ 16 | success: false, 17 | error: "Email, password, and name are required" 18 | }, { status: 400 }) 19 | } 20 | 21 | // Validate email format 22 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ 23 | if (!emailRegex.test(email)) { 24 | return NextResponse.json({ 25 | success: false, 26 | error: "Invalid email format" 27 | }, { status: 400 }) 28 | } 29 | 30 | // Validate password strength 31 | if (password.length < 6) { 32 | return NextResponse.json({ 33 | success: false, 34 | error: "Password must be at least 6 characters long" 35 | }, { status: 400 }) 36 | } 37 | 38 | // Check if users table exists, create if not 39 | const [tables] = await pool.query( 40 | "SHOW TABLES LIKE 'users'" 41 | ) 42 | 43 | if (!Array.isArray(tables) || tables.length === 0) { 44 | // Create users table 45 | await pool.query(` 46 | CREATE TABLE IF NOT EXISTS users ( 47 | id INT AUTO_INCREMENT PRIMARY KEY, 48 | email VARCHAR(255) NOT NULL UNIQUE, 49 | password_hash VARCHAR(255) NOT NULL, 50 | name VARCHAR(255) DEFAULT NULL, 51 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 52 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 53 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 54 | `) 55 | } 56 | 57 | // SECURITY CHECK: Ensure no users exist before allowing registration 58 | const [existingUsers] = await pool.query( 59 | "SELECT COUNT(*) as count FROM users" 60 | ) 61 | 62 | const userCount = Array.isArray(existingUsers) && existingUsers.length > 0 ? existingUsers[0].count : 0 63 | 64 | if (userCount > 0) { 65 | return NextResponse.json({ 66 | success: false, 67 | error: "Registration is only allowed when no users exist. Please use login instead." 68 | }, { status: 403 }) 69 | } 70 | 71 | // Check if email already exists (extra safety) 72 | const [emailCheck] = await pool.query( 73 | "SELECT id FROM users WHERE email = ? LIMIT 1", 74 | [email] 75 | ) 76 | 77 | if (Array.isArray(emailCheck) && emailCheck.length > 0) { 78 | return NextResponse.json({ 79 | success: false, 80 | error: "Email already exists" 81 | }, { status: 400 }) 82 | } 83 | 84 | // Hash password 85 | const hashedPassword = await bcrypt.hash(password, 12) 86 | 87 | // Create first user 88 | await pool.query( 89 | "INSERT INTO users (email, password_hash, name) VALUES (?, ?, ?)", 90 | [email, hashedPassword, name] 91 | ) 92 | 93 | console.log("✅ First user created successfully:", email) 94 | 95 | return NextResponse.json({ 96 | success: true, 97 | message: "First user created successfully. You can now login.", 98 | user: { 99 | email: email, 100 | name: name 101 | } 102 | }) 103 | } catch (err) { 104 | console.error("Register first user error:", err) 105 | 106 | // Handle duplicate email error 107 | if (err instanceof Error && err.message.includes('Duplicate entry')) { 108 | return NextResponse.json({ 109 | success: false, 110 | error: "Email already exists" 111 | }, { status: 400 }) 112 | } 113 | 114 | return NextResponse.json({ 115 | success: false, 116 | error: "Failed to create user", 117 | details: err instanceof Error ? err.message : "Unknown error" 118 | }, { status: 500 }) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/api/browser-analysis/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { pool } from "@/lib/mysql"; 3 | import type { RowDataPacket } from "mysql2"; 4 | import { validateRequest } from "@/lib/auth"; 5 | 6 | interface BrowserData { 7 | browser: string; 8 | count: number; 9 | } 10 | 11 | export async function GET(request: NextRequest) { 12 | // Validate authentication 13 | const user = await validateRequest(request) 14 | if (!user) { 15 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 16 | } 17 | 18 | try { 19 | // Query to get unique browsers per device_id 20 | const [results] = await pool.query(` 21 | SELECT DISTINCT device_id, browser 22 | FROM credentials 23 | WHERE browser IS NOT NULL AND browser != '' 24 | ORDER BY device_id, browser 25 | `); 26 | 27 | if (!Array.isArray(results)) { 28 | return NextResponse.json({ success: false, error: "Invalid data format" }, { status: 500 }); 29 | } 30 | 31 | // Process and normalize browser names 32 | const browserCounts: { [key: string]: number } = {}; 33 | 34 | results.forEach((row) => { 35 | const originalBrowser = row.browser; 36 | if (!originalBrowser) return; 37 | 38 | // Normalize browser name 39 | let normalizedBrowser = originalBrowser 40 | .toLowerCase() 41 | .replace(/\s*\([^)]*\)/g, '') // Remove version info in parentheses 42 | .replace(/\s*profile\s*\d*/gi, '') // Remove "Profile X" 43 | .replace(/\s*default/gi, '') // Remove "Default" 44 | .replace(/\s*\([^)]*\)/g, '') // Remove any remaining parentheses content 45 | .trim(); 46 | 47 | // Map common browser names 48 | if (normalizedBrowser.includes('chrome') && !normalizedBrowser.includes('chromium')) { 49 | normalizedBrowser = 'Google Chrome'; 50 | } else if (normalizedBrowser.includes('edge') || normalizedBrowser.includes('microsoft')) { 51 | normalizedBrowser = 'Microsoft Edge'; 52 | } else if (normalizedBrowser.includes('firefox') || normalizedBrowser.includes('mozilla')) { 53 | normalizedBrowser = 'Mozilla Firefox'; 54 | } else if (normalizedBrowser.includes('safari')) { 55 | normalizedBrowser = 'Safari'; 56 | } else if (normalizedBrowser.includes('opera')) { 57 | normalizedBrowser = 'Opera'; 58 | } else if (normalizedBrowser.includes('brave')) { 59 | normalizedBrowser = 'Brave'; 60 | } else if (normalizedBrowser.includes('chromium')) { 61 | normalizedBrowser = 'Chromium'; 62 | } else { 63 | // Capitalize first letter of each word for unknown browsers 64 | normalizedBrowser = normalizedBrowser 65 | .split(' ') 66 | .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1)) 67 | .join(' '); 68 | } 69 | 70 | // Count unique browsers per device 71 | if (normalizedBrowser) { 72 | browserCounts[normalizedBrowser] = (browserCounts[normalizedBrowser] || 0) + 1; 73 | } 74 | }); 75 | 76 | // Convert to array and sort by count 77 | const browserAnalysis: BrowserData[] = Object.entries(browserCounts) 78 | .map(([browser, count]) => ({ browser, count })) 79 | .sort((a, b) => b.count - a.count) 80 | .slice(0, 10); // Top 10 browsers 81 | 82 | return NextResponse.json({ 83 | success: true, 84 | browserAnalysis 85 | }); 86 | 87 | } catch (error) { 88 | console.error("Browser analysis error:", error); 89 | return NextResponse.json({ 90 | success: false, 91 | error: "Internal server error" 92 | }, { status: 500 }); 93 | } 94 | } -------------------------------------------------------------------------------- /app/api/device-credentials/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { executeQuery } from "@/lib/mysql" 3 | import { logInfo, logError } from "@/lib/logger" 4 | import { deviceCredentialsSchema, validateData, createValidationErrorResponse } from "@/lib/validation" 5 | import { validateRequest } from "@/lib/auth" 6 | 7 | export async function POST(request: NextRequest) { 8 | // Validate authentication 9 | const user = await validateRequest(request) 10 | if (!user) { 11 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 12 | } 13 | 14 | try { 15 | const body = await request.json() 16 | 17 | // Validate input 18 | const validation = validateData(deviceCredentialsSchema, body) 19 | if (!validation.success) { 20 | logError("Validation failed for device credentials request", validation.errors, 'Device Credentials API') 21 | return NextResponse.json(createValidationErrorResponse(validation.errors), { status: 400 }) 22 | } 23 | 24 | const { deviceId } = validation.data 25 | logInfo(`Loading credentials for device: ${deviceId}`, undefined, 'Device Credentials API') 26 | 27 | // First, verify the device exists 28 | const deviceCheck = (await executeQuery("SELECT device_id, device_name FROM devices WHERE device_id = ?", [ 29 | deviceId, 30 | ])) as any[] 31 | 32 | console.log("📱 Device check result:", deviceCheck) 33 | 34 | if (deviceCheck.length === 0) { 35 | console.log("❌ Device not found:", deviceId) 36 | return NextResponse.json({ error: "Device not found" }, { status: 404 }) 37 | } 38 | 39 | // ENHANCED: Get credentials with flexible browser handling 40 | const credentials = (await executeQuery( 41 | `SELECT 42 | COALESCE(browser, 'Unknown') as browser, 43 | COALESCE(url, '') as url, 44 | COALESCE(username, '') as username, 45 | COALESCE(password, '') as password, 46 | file_path 47 | FROM credentials 48 | WHERE device_id = ? 49 | ORDER BY url, username`, 50 | [deviceId], 51 | )) as any[] 52 | 53 | console.log(`📊 Found ${credentials.length} credentials for device ${deviceId}`) 54 | 55 | // Debug: Check total credentials in database 56 | const totalCredentials = (await executeQuery("SELECT COUNT(*) as count FROM credentials")) as any[] 57 | console.log(`📊 Total credentials in database: ${JSON.stringify(totalCredentials)}`) 58 | 59 | // Debug: Check credentials with device info 60 | const credentialsWithDevice = (await executeQuery( 61 | `SELECT c.*, d.device_name 62 | FROM credentials c 63 | JOIN devices d ON c.device_id = d.device_id 64 | WHERE c.device_id = ?`, 65 | [deviceId], 66 | )) as any[] 67 | 68 | console.log(`🔍 Credential details with device info: ${JSON.stringify(credentialsWithDevice)}`) 69 | 70 | // Format credentials for display - Handle missing browser gracefully 71 | const formattedCredentials = credentials.map((cred) => ({ 72 | browser: cred.browser === "Unknown" || !cred.browser ? null : cred.browser, 73 | url: cred.url || "", 74 | username: cred.username || "", 75 | password: cred.password || "", 76 | filePath: cred.file_path || "", 77 | })) 78 | 79 | console.log(`✅ Returning ${formattedCredentials.length} formatted credentials`) 80 | 81 | return NextResponse.json(formattedCredentials) 82 | } catch (error) { 83 | console.error("❌ Error loading device credentials:", error) 84 | return NextResponse.json( 85 | { 86 | error: "Failed to load credentials", 87 | details: error instanceof Error ? error.message : "Unknown error", 88 | }, 89 | { status: 500 }, 90 | ) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /app/api/file-content/route.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest, NextResponse } from "next/server" 2 | import { executeQuery } from "@/lib/mysql" 3 | import { existsSync, createReadStream } from "fs" 4 | import path from "path" 5 | import { validateRequest } from "@/lib/auth" 6 | import { Readable } from "stream" 7 | 8 | export const runtime = "nodejs" 9 | 10 | function nodeStreamToWeb(stream: Readable): ReadableStream { 11 | // Node 17+ includes toWeb 12 | // @ts-ignore 13 | return (stream as any).toWeb?.() ?? new ReadableStream({ 14 | start(controller) { 15 | stream.on("data", (chunk) => controller.enqueue(chunk)) 16 | stream.on("end", () => controller.close()) 17 | stream.on("error", (err) => controller.error(err)) 18 | }, 19 | cancel() { 20 | stream.destroy() 21 | } 22 | }) 23 | } 24 | 25 | export async function POST(request: NextRequest) { 26 | const user = await validateRequest(request) 27 | if (!user) { 28 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 29 | } 30 | 31 | try { 32 | const { deviceId, filePath } = await request.json() 33 | 34 | if (!deviceId || !filePath) { 35 | return NextResponse.json({ error: "Device ID and file path are required" }, { status: 400 }) 36 | } 37 | 38 | let results = await executeQuery( 39 | "SELECT content FROM files WHERE device_id = ? AND file_path = ? AND content IS NOT NULL", 40 | [deviceId, filePath], 41 | ) 42 | 43 | if (results && (results as any[]).length > 0) { 44 | const content = (results as any[])[0].content 45 | return NextResponse.json({ content }) 46 | } 47 | 48 | results = await executeQuery( 49 | "SELECT local_file_path FROM files WHERE device_id = ? AND file_path = ? AND local_file_path IS NOT NULL", 50 | [deviceId, filePath], 51 | ) 52 | 53 | if (!results || (results as any[]).length === 0) { 54 | return NextResponse.json({ error: "File not found or not readable" }, { status: 404 }) 55 | } 56 | 57 | const localFilePath = (results as any[])[0].local_file_path 58 | const absPath = path.isAbsolute(localFilePath) 59 | ? localFilePath 60 | : path.join(process.cwd(), localFilePath) 61 | 62 | if (!existsSync(absPath)) { 63 | return NextResponse.json({ error: "File not found on disk" }, { status: 404 }) 64 | } 65 | 66 | const ext = path.extname(absPath).toLowerCase() 67 | let contentType = "application/octet-stream" 68 | if ([".jpg", ".jpeg"].includes(ext)) contentType = "image/jpeg" 69 | else if (ext === ".png") contentType = "image/png" 70 | else if (ext === ".gif") contentType = "image/gif" 71 | else if (ext === ".bmp") contentType = "image/bmp" 72 | else if (ext === ".webp") contentType = "image/webp" 73 | 74 | const nodeStream = createReadStream(absPath) 75 | const webStream = nodeStreamToWeb(nodeStream) 76 | 77 | return new NextResponse(webStream, { 78 | status: 200, 79 | headers: { 80 | "Content-Type": contentType, 81 | "Content-Disposition": `inline; filename="${path.basename(absPath)}"`, 82 | }, 83 | }) 84 | } catch (error) { 85 | console.error("File content error:", error) 86 | return NextResponse.json( 87 | { 88 | error: "Failed to get file content", 89 | details: error instanceof Error ? error.message : "Unknown error", 90 | }, 91 | { status: 500 }, 92 | ) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest, NextResponse } from "next/server" 2 | import { executeQuery } from "@/lib/mysql" 3 | import { validateRequest } from "@/lib/auth" 4 | 5 | export async function POST(request: NextRequest) { 6 | // Validate authentication 7 | const user = await validateRequest(request) 8 | if (!user) { 9 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 10 | } 11 | 12 | try { 13 | const { query, type } = await request.json() 14 | 15 | if (!query || !type) { 16 | return NextResponse.json({ error: "Query and type are required" }, { status: 400 }) 17 | } 18 | 19 | console.log(`🔍 Searching for: "${query}" by ${type}`) 20 | 21 | let searchResults: any[] 22 | 23 | if (type === "email") { 24 | // Search for email in file content - look in ALL text files, not just credentials table 25 | searchResults = await executeQuery( 26 | ` 27 | SELECT 28 | d.device_id, 29 | d.device_name, 30 | d.upload_batch, 31 | d.upload_date, 32 | c.username, 33 | c.url, 34 | c.password, 35 | c.browser, 36 | c.file_path 37 | FROM devices d 38 | JOIN credentials c ON d.device_id = c.device_id 39 | WHERE c.username LIKE ? 40 | ORDER BY d.upload_date DESC, d.device_name, c.url 41 | `, 42 | [`%${query}%`], 43 | ) as any[] 44 | } else if (type === "domain") { 45 | // Search for domain in file content - look in ALL text files 46 | searchResults = await executeQuery( 47 | ` 48 | SELECT 49 | d.device_id, 50 | d.device_name, 51 | d.upload_batch, 52 | d.upload_date, 53 | c.username, 54 | c.url, 55 | c.password, 56 | c.browser, 57 | c.file_path 58 | FROM devices d 59 | JOIN credentials c ON d.device_id = c.device_id 60 | WHERE c.url LIKE ? OR c.domain LIKE ? 61 | ORDER BY d.upload_date DESC, d.device_name, c.url 62 | `, 63 | [`%${query}%`, `%${query}%`], 64 | ) as any[] 65 | } else { 66 | return NextResponse.json({ error: "Invalid search type" }, { status: 400 }) 67 | } 68 | 69 | console.log(`📊 Raw search results: ${searchResults.length} credential matches`) 70 | 71 | // Group results by device 72 | const deviceMap = new Map() 73 | 74 | for (const row of searchResults) { 75 | if (!deviceMap.has(row.device_id)) { 76 | deviceMap.set(row.device_id, { 77 | deviceId: row.device_id, 78 | deviceName: row.device_name, 79 | uploadBatch: row.upload_batch, 80 | uploadDate: row.upload_date, 81 | matchingFiles: [], 82 | matchedContent: [], 83 | files: [], 84 | totalFiles: 0, 85 | credentials: [], // Add credentials to show what matched 86 | }) 87 | } 88 | 89 | const device = deviceMap.get(row.device_id) 90 | 91 | // Add matching credential info 92 | device.credentials.push({ 93 | username: row.username, 94 | url: row.url, 95 | password: row.password, 96 | browser: row.browser, 97 | filePath: row.file_path, 98 | }) 99 | 100 | // Add file path to matching files if not already there 101 | if (row.file_path && !device.matchingFiles.includes(row.file_path)) { 102 | device.matchingFiles.push(row.file_path) 103 | } 104 | 105 | // Add matched content for display 106 | const matchedLine = `${row.username} - ${row.url}` 107 | if (!device.matchedContent.includes(matchedLine)) { 108 | device.matchedContent.push(matchedLine) 109 | } 110 | } 111 | 112 | console.log(`📊 Grouped by devices: ${deviceMap.size} devices found`) 113 | 114 | // Get complete file list for each matching device 115 | for (const [deviceId, device] of deviceMap) { 116 | const allFiles = await executeQuery( 117 | ` 118 | SELECT file_path, file_name, parent_path, is_directory, file_size, 119 | CASE WHEN content IS NOT NULL OR local_file_path IS NOT NULL THEN 1 ELSE 0 END as has_content 120 | FROM files 121 | WHERE device_id = ? 122 | ORDER BY file_path 123 | `, 124 | [deviceId], 125 | ) as any[] 126 | 127 | device.files = allFiles 128 | device.totalFiles = (allFiles as any[]).length 129 | } 130 | 131 | const results = Array.from(deviceMap.values()) 132 | 133 | console.log(`✅ Final search results: ${results.length} devices with matches`) 134 | 135 | return NextResponse.json(results) 136 | } catch (error) { 137 | console.error("❌ Search error:", error) 138 | return NextResponse.json( 139 | { 140 | error: "Search failed", 141 | details: error instanceof Error ? error.message : "Unknown error", 142 | }, 143 | { status: 500 }, 144 | ) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/api/software-analysis/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import { pool } from "@/lib/mysql"; 3 | import type { RowDataPacket } from "mysql2"; 4 | import { validateRequest } from "@/lib/auth"; 5 | 6 | interface SoftwareData { 7 | software_name: string; 8 | version: string | null; 9 | count: number; 10 | } 11 | 12 | export async function GET(request: NextRequest) { 13 | // Validate authentication 14 | const user = await validateRequest(request) 15 | if (!user) { 16 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 17 | } 18 | 19 | try { 20 | // Query to get software grouped by name and version for attack surface management 21 | const [results] = await pool.query(` 22 | SELECT software_name, version, COUNT(DISTINCT device_id) as count 23 | FROM software 24 | WHERE software_name IS NOT NULL AND software_name != '' 25 | GROUP BY software_name, version 26 | ORDER BY count DESC, software_name, version 27 | LIMIT 10 28 | `); 29 | 30 | if (!Array.isArray(results)) { 31 | return NextResponse.json({ success: false, error: "Invalid data format" }, { status: 500 }); 32 | } 33 | 34 | // Convert to array format 35 | const softwareAnalysis: SoftwareData[] = results.map((row) => ({ 36 | software_name: row.software_name, 37 | version: row.version, 38 | count: row.count 39 | })); 40 | 41 | return NextResponse.json({ 42 | success: true, 43 | softwareAnalysis 44 | }); 45 | 46 | } catch (error) { 47 | console.error("Software analysis error:", error); 48 | return NextResponse.json({ 49 | success: false, 50 | error: "Internal server error" 51 | }, { status: 500 }); 52 | } 53 | } -------------------------------------------------------------------------------- /app/api/top-tlds/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server" 2 | import { executeQuery } from "@/lib/mysql" 3 | import { validateRequest } from "@/lib/auth" 4 | 5 | export async function GET(request: NextRequest) { 6 | console.log("🔍 [TOP-TLDS] API called") 7 | 8 | // Validate authentication 9 | const user = await validateRequest(request) 10 | console.log("🔍 [TOP-TLDS] Auth validation result:", user ? "SUCCESS" : "FAILED") 11 | 12 | if (!user) { 13 | console.log("❌ [TOP-TLDS] Unauthorized - no valid user found") 14 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 15 | } 16 | 17 | try { 18 | console.log("📊 [TOP-TLDS] Loading top TLDs for user:", (user as any).username || "") 19 | 20 | // Check cache first 21 | console.log("📊 [TOP-TLDS] Checking cache...") 22 | const cacheResult = (await executeQuery( 23 | "SELECT cache_data FROM analytics_cache WHERE cache_key = 'top_tlds' AND expires_at > NOW()", 24 | )) as any[] 25 | 26 | console.log("📊 [TOP-TLDS] Cache result length:", Array.isArray(cacheResult) ? cacheResult.length : "unexpected") 27 | 28 | if (cacheResult.length > 0) { 29 | console.log("📊 [TOP-TLDS] Using cached top TLDs") 30 | let cachedDataRaw = cacheResult[0].cache_data 31 | let cachedData: any = null 32 | 33 | try { 34 | if (typeof cachedDataRaw === "string") { 35 | cachedData = JSON.parse(cachedDataRaw) 36 | } else if (typeof cachedDataRaw === "object" && cachedDataRaw !== null) { 37 | // Already an object (possibly due to previous bad write), use as-is 38 | cachedData = cachedDataRaw 39 | } else { 40 | throw new Error("Unsupported cache_data type") 41 | } 42 | } catch (e) { 43 | console.warn("📊 [TOP-TLDS] Failed to parse cached data, ignoring cache:", e) 44 | cachedData = null 45 | } 46 | 47 | if (cachedData) { 48 | console.log( 49 | "📊 [TOP-TLDS] Cached data length:", 50 | Array.isArray(cachedData) ? cachedData.length : "unknown", 51 | ) 52 | return NextResponse.json(cachedData) 53 | } else { 54 | console.log("📊 [TOP-TLDS] Cache corrupted or invalid, will recalc") 55 | } 56 | } 57 | 58 | console.log("📊 [TOP-TLDS] Calculating fresh top TLDs...") 59 | 60 | // Get top TLDs from credentials table 61 | const topTlds = await executeQuery(` 62 | SELECT 63 | tld, 64 | COUNT(*) as count, 65 | COUNT(DISTINCT device_id) as affected_devices 66 | FROM credentials 67 | WHERE tld IS NOT NULL 68 | AND tld != '' 69 | AND tld NOT LIKE '%localhost%' 70 | AND tld NOT LIKE '%127.0.0.1%' 71 | AND tld NOT LIKE '%192.168%' 72 | AND tld NOT LIKE '%10.%' 73 | GROUP BY tld 74 | ORDER BY count DESC, affected_devices DESC 75 | LIMIT 10 76 | `) 77 | 78 | console.log( 79 | `📊 [TOP-TLDS] Found ${Array.isArray(topTlds) ? (topTlds as any[]).length : "?"} top TLDs`, 80 | ) 81 | console.log("📊 [TOP-TLDS] Sample data:", Array.isArray(topTlds) ? (topTlds as any[]).slice(0, 2) : topTlds) 82 | 83 | // Serialize for cache (safe fallback) 84 | let serialized: string 85 | try { 86 | serialized = JSON.stringify(topTlds) 87 | } catch (e) { 88 | console.error("📊 [TOP-TLDS] Failed to serialize topTlds for cache:", e) 89 | serialized = "[]" // fallback empty array 90 | } 91 | 92 | // Cache for 10 minutes (upsert) 93 | console.log("📊 [TOP-TLDS] Caching results...") 94 | await executeQuery( 95 | ` 96 | INSERT INTO analytics_cache (cache_key, cache_data, expires_at) 97 | VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 10 MINUTE)) 98 | ON DUPLICATE KEY UPDATE cache_data = VALUES(cache_data), expires_at = VALUES(expires_at) 99 | `, 100 | ["top_tlds", serialized], 101 | ) 102 | 103 | console.log("📊 [TOP-TLDS] Returning fresh data") 104 | return NextResponse.json(topTlds) 105 | } catch (error) { 106 | console.error("❌ [TOP-TLDS] Error:", error) 107 | return NextResponse.json( 108 | { 109 | error: "Failed to get top TLDs", 110 | details: error instanceof Error ? error.message : "Unknown error", 111 | }, 112 | { status: 500 }, 113 | ) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /app/api/upload-logs/route.ts: -------------------------------------------------------------------------------- 1 | import type { NextRequest } from "next/server" 2 | import { connections } from "@/lib/upload-connections" 3 | import { validateRequest } from "@/lib/auth" 4 | import { NextResponse } from "next/server" 5 | 6 | export async function GET(request: NextRequest) { 7 | // Validate authentication 8 | const user = await validateRequest(request) 9 | if (!user) { 10 | return NextResponse.json({ success: false, error: "Unauthorized" }, { status: 401 }) 11 | } 12 | 13 | const { searchParams } = new URL(request.url) 14 | const sessionId = searchParams.get("sessionId") || "default" 15 | 16 | const stream = new ReadableStream({ 17 | start(controller) { 18 | // Store this connection 19 | connections.set(sessionId, controller) 20 | console.log(`🔗 Connection stored for session: ${sessionId}`) 21 | console.log(`📊 Total connections now: ${connections.size}`) 22 | console.log(`📋 All sessions: ${Array.from(connections.keys())}`) 23 | 24 | // Send initial connection message 25 | const data = `data: ${JSON.stringify({ 26 | timestamp: new Date().toISOString(), 27 | message: "🔗 Connected to upload log stream", 28 | type: "info", 29 | })}\n\n` 30 | 31 | controller.enqueue(new TextEncoder().encode(data)) 32 | 33 | // Send heartbeat to ensure connection is established 34 | setTimeout(() => { 35 | if (connections.has(sessionId)) { 36 | const heartbeat = `data: ${JSON.stringify({ 37 | timestamp: new Date().toISOString(), 38 | message: "✅ Connection established, ready for upload logs", 39 | type: "info", 40 | })}\n\n` 41 | 42 | try { 43 | controller.enqueue(new TextEncoder().encode(heartbeat)) 44 | } catch (error) { 45 | console.error('Failed to send heartbeat:', error) 46 | connections.delete(sessionId) 47 | } 48 | } 49 | }, 100) // Send heartbeat after 100ms 50 | }, 51 | cancel() { 52 | // Clean up connection 53 | connections.delete(sessionId) 54 | }, 55 | }) 56 | 57 | return new Response(stream, { 58 | headers: { 59 | "Content-Type": "text/event-stream", 60 | "Cache-Control": "no-cache", 61 | Connection: "keep-alive", 62 | "Access-Control-Allow-Origin": "*", 63 | "Access-Control-Allow-Headers": "Cache-Control", 64 | }, 65 | }) 66 | } 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/components/logout-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { Button } from "@/components/ui/button"; 3 | import { useRouter } from "next/navigation"; 4 | import { useState } from "react"; 5 | 6 | export default function LogoutButton() { 7 | const router = useRouter(); 8 | const [loading, setLoading] = useState(false); 9 | 10 | const handleLogout = async () => { 11 | setLoading(true); 12 | await fetch("/api/auth/logout", { 13 | method: "POST", 14 | credentials: "include", 15 | }); 16 | setLoading(false); 17 | router.replace("/login"); 18 | }; 19 | 20 | return ( 21 | 24 | ); 25 | } -------------------------------------------------------------------------------- /app/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return null 3 | } 4 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return null 3 | } 4 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/animated-counter.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import { motion, useInView } from "framer-motion" 5 | import { useRef } from "react" 6 | 7 | interface AnimatedCounterProps { 8 | value: number 9 | duration?: number 10 | delay?: number 11 | className?: string 12 | } 13 | 14 | export function AnimatedCounter({ 15 | value, 16 | duration = 2, 17 | delay = 0, 18 | className = "" 19 | }: AnimatedCounterProps) { 20 | const [count, setCount] = useState(0) 21 | const ref = useRef(null) 22 | const isInView = useInView(ref, { once: true, margin: "-100px" }) 23 | 24 | // Safety check for value 25 | const safeValue = typeof value === 'number' && !isNaN(value) ? value : 0 26 | 27 | useEffect(() => { 28 | if (!isInView) return 29 | 30 | let startTime: number 31 | let animationFrame: number 32 | let isCancelled = false 33 | 34 | const animate = (timestamp: number) => { 35 | if (isCancelled) return 36 | 37 | if (!startTime) startTime = timestamp + delay * 1000 38 | 39 | if (timestamp < startTime) { 40 | animationFrame = requestAnimationFrame(animate) 41 | return 42 | } 43 | 44 | const progress = Math.min((timestamp - startTime) / (duration * 1000), 1) 45 | 46 | // Easing function for smooth animation 47 | const easeOutQuart = 1 - Math.pow(1 - progress, 4) 48 | 49 | if (!isCancelled) { 50 | setCount(Math.floor(easeOutQuart * safeValue)) 51 | } 52 | 53 | if (progress < 1 && !isCancelled) { 54 | animationFrame = requestAnimationFrame(animate) 55 | } else if (!isCancelled) { 56 | setCount(safeValue) 57 | } 58 | } 59 | 60 | animationFrame = requestAnimationFrame(animate) 61 | 62 | return () => { 63 | isCancelled = true 64 | if (animationFrame) { 65 | cancelAnimationFrame(animationFrame) 66 | } 67 | } 68 | }, [safeValue, duration, delay, isInView]) 69 | 70 | return ( 71 | 83 | {count.toLocaleString()} 84 | 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /components/animated-software-list.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { motion, useInView } from "framer-motion" 4 | import { useRef } from "react" 5 | 6 | interface SoftwareData { 7 | software_name: string 8 | version: string | null 9 | count: number 10 | } 11 | 12 | interface AnimatedSoftwareListProps { 13 | softwareData: SoftwareData[] 14 | } 15 | 16 | export function AnimatedSoftwareList({ softwareData }: AnimatedSoftwareListProps) { 17 | const ref = useRef(null) 18 | const isInView = useInView(ref, { once: true, margin: "-50px" }) 19 | 20 | // Safety check for softwareData 21 | const safeSoftwareData = Array.isArray(softwareData) ? softwareData : [] 22 | 23 | if (safeSoftwareData.length === 0) { 24 | return ( 25 |
26 |

No software data available

27 |

Upload some stealer logs to see software statistics

28 |
29 | ) 30 | } 31 | 32 | const maxCount = Math.max(...safeSoftwareData.map(s => s?.count || 0)) 33 | 34 | return ( 35 |
36 | {safeSoftwareData.map((item, index) => { 37 | const percentage = maxCount > 0 ? ((item?.count || 0) / maxCount) * 100 : 0 38 | 39 | return ( 40 |
44 |
45 |
46 |
47 | #{index + 1} 48 |
49 |
50 |
51 | 52 | {item?.software_name || 'Unknown Software'} 53 | 54 | {item?.version && ( 55 | 56 | {item.version} 57 | 58 | )} 59 |
60 |
61 |
62 | 63 | {item?.count || 0} 64 | 65 |
66 |
67 | 78 |
79 |
80 | ) 81 | })} 82 |
83 | ) 84 | } 85 | -------------------------------------------------------------------------------- /components/animated-stat-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Card, CardContent } from "@/components/ui/card" 4 | import { AnimatedCounter } from "./animated-counter" 5 | import { LucideIcon } from "lucide-react" 6 | 7 | interface AnimatedStatCardProps { 8 | icon: LucideIcon 9 | value: number 10 | label: string 11 | iconColor: string 12 | delay?: number 13 | } 14 | 15 | export function AnimatedStatCard({ 16 | icon: Icon, 17 | value, 18 | label, 19 | iconColor, 20 | delay = 0 21 | }: AnimatedStatCardProps) { 22 | return ( 23 | 24 | 25 | 26 |
27 | 33 |

34 | {label} 35 |

36 |
37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /components/app-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import UserProfileDropdown from "./user-profile-dropdown"; 4 | import { SidebarTrigger } from "@/components/ui/sidebar"; 5 | import ErrorBoundary from "./error-boundary"; 6 | import ForceRefreshWrapper from "./force-refresh-wrapper"; 7 | 8 | interface AppHeaderProps { 9 | title?: string; 10 | } 11 | 12 | export default function AppHeader({ title }: AppHeaderProps) { 13 | return ( 14 |
15 |
16 |
17 | 18 |

{title || 'broń Vault'}

19 |
20 |
21 | 22 | Profile error
}> 23 | 24 | 25 | 26 |
27 | 28 |
29 | ); 30 | } -------------------------------------------------------------------------------- /components/app-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Search, Upload, BarChart3, Bug } from "lucide-react" 4 | import Link from "next/link" 5 | import { usePathname } from "next/navigation" 6 | import { useTheme } from "next-themes" 7 | import { Sun, Moon } from "lucide-react" 8 | import { Switch } from "@/components/ui/switch" 9 | import React from "react" 10 | 11 | import { 12 | Sidebar, 13 | SidebarContent, 14 | SidebarGroup, 15 | SidebarGroupContent, 16 | SidebarGroupLabel, 17 | SidebarMenu, 18 | SidebarMenuButton, 19 | SidebarMenuItem, 20 | SidebarHeader, 21 | } from "@/components/ui/sidebar" 22 | 23 | 24 | 25 | const menuItems = [ 26 | { 27 | title: "Dashboard", 28 | url: "/dashboard", 29 | icon: BarChart3, 30 | }, 31 | { 32 | title: "Search", 33 | url: "/", 34 | icon: Search, 35 | }, 36 | { 37 | title: "Upload", 38 | url: "/upload", 39 | icon: Upload, 40 | }, 41 | { 42 | title: "Debug ZIP", 43 | url: "/debug-zip", 44 | icon: Bug, 45 | }, 46 | ] 47 | 48 | export function AppSidebar() { 49 | const pathname = usePathname(); 50 | const { resolvedTheme, setTheme } = useTheme(); 51 | const [mounted, setMounted] = React.useState(false); 52 | const [logoSrc, setLogoSrc] = React.useState("/images/logo.png"); 53 | 54 | React.useEffect(() => { 55 | setMounted(true); 56 | }, []); 57 | 58 | React.useEffect(() => { 59 | if (mounted) { 60 | const timestamp = new Date().getTime(); 61 | const logo = resolvedTheme === 'light' ? "/images/logo-light.png" : "/images/logo.png"; 62 | setLogoSrc(`${logo}?t=${timestamp}`); 63 | } 64 | }, [mounted, resolvedTheme]); 65 | 66 | return ( 67 | 68 | 69 |
70 | broń Vault Logo 75 |

76 | Where stolen data meets structured investigation. 77 |

78 |
79 |
80 | 81 | 82 | Navigation 83 | 84 | 85 | {menuItems.map((item) => ( 86 | 87 | 95 | 96 | 97 | {item.title} 98 | 99 | 100 | 101 | ))} 102 | 103 | 104 | 105 |
106 |
107 | 108 | 109 | Light 110 | 111 | {mounted && ( 112 | setTheme(checked ? 'dark' : 'light')} 115 | aria-label="Toggle theme" 116 | className="data-[state=checked]:bg-[var(--bron-accent-red)] bg-[var(--bron-bg-tertiary)] border-[var(--bron-border)] 117 | [&>span]:bg-[var(--bron-text-primary)]" 118 | /> 119 | )} 120 | 121 | 122 | Dark 123 | 124 |
125 | 126 | 127 | 128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /components/auth-guard.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useAuth } from '@/hooks/useAuth' 4 | 5 | interface AuthGuardProps { 6 | children: React.ReactNode 7 | fallback?: React.ReactNode 8 | } 9 | 10 | export function AuthGuard({ children, fallback }: AuthGuardProps) { 11 | const { user, loading, error } = useAuth(true) 12 | 13 | if (loading) { 14 | return ( 15 | fallback || ( 16 |
17 |
18 |
19 |

Checking authentication...

20 |
21 |
22 | ) 23 | ) 24 | } 25 | 26 | if (error || !user) { 27 | return ( 28 | fallback || ( 29 |
30 |
31 |

Authentication required

32 |

Redirecting to login...

33 |
34 |
35 | ) 36 | ) 37 | } 38 | 39 | return <>{children} 40 | } 41 | -------------------------------------------------------------------------------- /components/client-layout-with-sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { usePathname } from "next/navigation"; 3 | import { useEffect, useState } from "react"; 4 | import { AppSidebar } from "@/components/app-sidebar"; 5 | import AppHeader from "@/components/app-header"; 6 | 7 | export default function ClientLayoutWithSidebar({ children }: { children: React.ReactNode }) { 8 | const pathname = usePathname(); 9 | 10 | // Determine page title based on pathname 11 | let title = "broń Vault"; 12 | if (pathname === "/dashboard") title = "broń Vault - Dashboard"; 13 | else if (pathname === "/") title = "broń Vault - Search"; 14 | else if (pathname === "/upload") title = "broń Vault - Upload"; 15 | else if (pathname === "/debug-zip") title = "broń Vault - Debug ZIP"; 16 | 17 | // Don't render sidebar/header if on login page 18 | if (pathname === "/login") { 19 | return ( 20 |
{children}
21 | ); 22 | } 23 | 24 | return ( 25 | <> 26 | 27 |
28 | 29 |
{children}
30 |
31 | 32 | ); 33 | } -------------------------------------------------------------------------------- /components/device/DeviceDetailsPanel.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import { X, User, File } from "lucide-react" 5 | import { Button } from "@/components/ui/button" 6 | import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet" 7 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" 8 | import { ScrollArea } from "@/components/ui/scroll-area" 9 | import { CredentialsTable } from "./CredentialsTable" 10 | import { FileTreeViewer } from "../file/FileTreeViewer" 11 | 12 | interface SearchResult { 13 | deviceId: string 14 | deviceName: string 15 | uploadBatch: string 16 | matchingFiles: string[] 17 | matchedContent: string[] 18 | files: any[] 19 | totalFiles: number 20 | upload_date?: string 21 | uploadDate?: string 22 | } 23 | 24 | interface Credential { 25 | browser: string 26 | url: string 27 | username: string 28 | password: string 29 | filePath?: string 30 | } 31 | 32 | interface DeviceDetailsPanelProps { 33 | selectedDevice: SearchResult | null 34 | onClose: () => void 35 | deviceCredentials: Credential[] 36 | isLoadingCredentials: boolean 37 | credentialsError: string 38 | showPasswords: boolean 39 | setShowPasswords: (show: boolean) => void 40 | credentialsSearchQuery: string 41 | setCredentialsSearchQuery: (query: string) => void 42 | onRetryCredentials: () => void 43 | onFileClick: (deviceId: string, filePath: string, fileName: string, hasContent: boolean) => void 44 | onDownloadAllData: (deviceId: string, deviceName: string) => void 45 | } 46 | 47 | export function DeviceDetailsPanel({ 48 | selectedDevice, 49 | onClose, 50 | deviceCredentials, 51 | isLoadingCredentials, 52 | credentialsError, 53 | showPasswords, 54 | setShowPasswords, 55 | credentialsSearchQuery, 56 | setCredentialsSearchQuery, 57 | onRetryCredentials, 58 | onFileClick, 59 | onDownloadAllData, 60 | }: DeviceDetailsPanelProps) { 61 | if (!selectedDevice) return null 62 | 63 | return ( 64 | 65 | 66 | 67 | 68 | {selectedDevice.deviceName} 69 | 77 | 78 | 79 | 80 |
81 | 82 | 83 | 87 | 88 | User Credentials 89 | 90 | 94 | 95 | Supporting Files 96 | 97 | 98 | 99 | 100 | 101 | 112 | 113 | 114 | 115 | 116 | 117 | 122 | 123 | 124 | 125 |
126 |
127 |
128 | ) 129 | } 130 | -------------------------------------------------------------------------------- /components/error-boundary.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { logError } from "@/lib/logger"; 5 | 6 | interface ErrorBoundaryState { 7 | hasError: boolean; 8 | error?: Error; 9 | errorId?: string; 10 | } 11 | 12 | interface ErrorBoundaryProps { 13 | children: React.ReactNode; 14 | fallback?: React.ReactNode; 15 | context?: string; 16 | onError?: (error: Error, errorInfo: React.ErrorInfo) => void; 17 | } 18 | 19 | class ErrorBoundary extends React.Component { 20 | constructor(props: ErrorBoundaryProps) { 21 | super(props); 22 | this.state = { hasError: false }; 23 | } 24 | 25 | static getDerivedStateFromError(error: Error): ErrorBoundaryState { 26 | const errorId = `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; 27 | return { hasError: true, error, errorId }; 28 | } 29 | 30 | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { 31 | const context = this.props.context || 'ErrorBoundary'; 32 | 33 | // Log error with proper context 34 | logError( 35 | `${context} caught an error: ${error.message}`, 36 | { 37 | error: error.toString(), 38 | stack: error.stack, 39 | componentStack: errorInfo.componentStack, 40 | errorId: this.state.errorId 41 | }, 42 | context 43 | ); 44 | 45 | // Call custom error handler if provided 46 | this.props.onError?.(error, errorInfo); 47 | } 48 | 49 | render() { 50 | if (this.state.hasError) { 51 | if (this.props.fallback) { 52 | return this.props.fallback; 53 | } 54 | 55 | return ( 56 |
57 |
58 |

Something went wrong

59 |

60 | {this.state.error?.message || "An unexpected error occurred"} 61 |

62 | {process.env.NODE_ENV === 'development' && this.state.errorId && ( 63 |

64 | Error ID: {this.state.errorId} 65 |

66 | )} 67 | 73 |
74 |
75 | ); 76 | } 77 | 78 | return this.props.children; 79 | } 80 | } 81 | 82 | export default ErrorBoundary; 83 | -------------------------------------------------------------------------------- /components/file/FileContentDialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog" 5 | import { ScrollArea } from "@/components/ui/scroll-area" 6 | 7 | interface FileContentDialogProps { 8 | selectedFile: { deviceId: string; filePath: string; fileName: string } | null 9 | onClose: () => void 10 | fileContent: string 11 | isLoadingFile: boolean 12 | selectedFileType: 'text' | 'image' | null 13 | deviceName?: string 14 | } 15 | 16 | export function FileContentDialog({ 17 | selectedFile, 18 | onClose, 19 | fileContent, 20 | isLoadingFile, 21 | selectedFileType, 22 | deviceName, 23 | }: FileContentDialogProps) { 24 | if (!selectedFile) return null 25 | 26 | return ( 27 | 28 | 29 | 30 | {selectedFile.fileName} 31 | 32 | Device: {deviceName} 33 | 34 | 35 |
36 | {isLoadingFile ? ( 37 |
38 |

Loading file content...

39 |
40 | ) : selectedFileType === 'image' ? ( 41 |
42 | {selectedFile.fileName} 47 |
48 | ) : ( 49 |
50 |
51 |                 {fileContent}
52 |               
53 |
54 | )} 55 |
56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /components/force-refresh-wrapper.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | interface ForceRefreshWrapperProps { 6 | children: React.ReactNode; 7 | refreshKey?: string; 8 | } 9 | 10 | export default function ForceRefreshWrapper({ children, refreshKey }: ForceRefreshWrapperProps) { 11 | const [key, setKey] = useState(0); 12 | 13 | useEffect(() => { 14 | // Force refresh on mount and when refreshKey changes 15 | setKey(prev => prev + 1); 16 | }, [refreshKey]); 17 | 18 | useEffect(() => { 19 | // Force refresh on hot reload in development 20 | if (process.env.NODE_ENV === 'development' && typeof window !== 'undefined') { 21 | const handleBeforeUnload = () => { 22 | setKey(prev => prev + 1); 23 | }; 24 | 25 | // Add passive listener for better performance 26 | window.addEventListener('beforeunload', handleBeforeUnload, { passive: true }); 27 | 28 | return () => { 29 | window.removeEventListener('beforeunload', handleBeforeUnload); 30 | }; 31 | } 32 | }, []); 33 | 34 | return
{children}
; 35 | } 36 | -------------------------------------------------------------------------------- /components/search/SearchInterface.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import { Search, Mail, Globe } from "lucide-react" 5 | import { Button } from "@/components/ui/button" 6 | import { Input } from "@/components/ui/input" 7 | import { Card, CardContent } from "@/components/ui/card" 8 | import { Badge } from "@/components/ui/badge" 9 | import { generateId, announceToScreenReader } from "@/lib/accessibility" 10 | 11 | interface SearchInterfaceProps { 12 | searchQuery: string 13 | setSearchQuery: (query: string) => void 14 | searchType: "email" | "domain" 15 | setSearchType: (type: "email" | "domain") => void 16 | isLoading: boolean 17 | onSearch: () => void 18 | onDetectSearchType: (query: string) => void 19 | } 20 | 21 | export function SearchInterface({ 22 | searchQuery, 23 | setSearchQuery, 24 | searchType, 25 | setSearchType, 26 | isLoading, 27 | onSearch, 28 | onDetectSearchType, 29 | }: SearchInterfaceProps) { 30 | return ( 31 |
32 | 33 | 34 |
35 |
36 | 37 | { 42 | setSearchQuery(e.target.value) 43 | onDetectSearchType(e.target.value) 44 | }} 45 | onKeyPress={(e) => e.key === "Enter" && onSearch()} 46 | className="pl-10 h-12 text-lg bg-bron-bg-tertiary border-bron-border text-bron-text-primary placeholder:text-bron-text-muted" 47 | /> 48 |
49 | 56 |
57 |
58 | 68 | 69 | Email 70 | 71 | 81 | 82 | Domain 83 | 84 |
85 |
86 |
87 |
88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /components/search/TypingEffect.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState, useEffect } from "react" 4 | 5 | interface TypingEffectProps { 6 | sentences: string[] 7 | isVisible: boolean 8 | typingSpeed?: number 9 | pauseDuration?: number 10 | } 11 | 12 | export function TypingEffect({ 13 | sentences, 14 | isVisible, 15 | typingSpeed = 28, 16 | pauseDuration = 3000 17 | }: TypingEffectProps) { 18 | const [typingText, setTypingText] = useState("") 19 | const [typingIndex, setTypingIndex] = useState(0) 20 | 21 | useEffect(() => { 22 | if (!isVisible || sentences.length === 0) return 23 | 24 | setTypingText("") 25 | let i = 0 26 | let cancelled = false 27 | const fullText = sentences[typingIndex] 28 | 29 | function type() { 30 | if (cancelled) return 31 | if (i < fullText.length) { 32 | setTypingText(fullText.slice(0, i + 1)) 33 | i++ 34 | setTimeout(type, typingSpeed) 35 | } else { 36 | // Wait before switching to the next sentence 37 | setTimeout(() => { 38 | setTypingIndex((prev) => (prev + 1) % sentences.length) 39 | }, pauseDuration) 40 | } 41 | } 42 | 43 | type() 44 | return () => { 45 | cancelled = true 46 | } 47 | }, [isVisible, typingIndex, sentences, typingSpeed, pauseDuration]) 48 | 49 | if (!isVisible || sentences.length === 0) { 50 | return null 51 | } 52 | 53 | return ( 54 |
55 | 56 | {typeof typingText === "string" ? typingText.replace(/undefined/g, "") : ""} 57 | {typingText.length > 0 && typingText.length < sentences[typingIndex].length ? ( 58 | | 59 | ) : null} 60 | 61 |
62 | ) 63 | } 64 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import * as React from 'react' 4 | import { 5 | ThemeProvider as NextThemesProvider, 6 | type ThemeProviderProps, 7 | } from 'next-themes' 8 | 9 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 10 | return {children} 11 | } 12 | -------------------------------------------------------------------------------- /components/ui/accordion.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AccordionPrimitive from "@radix-ui/react-accordion" 5 | import { ChevronDown } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Accordion = AccordionPrimitive.Root 10 | 11 | const AccordionItem = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 20 | )) 21 | AccordionItem.displayName = "AccordionItem" 22 | 23 | const AccordionTrigger = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, children, ...props }, ref) => ( 27 | 28 | svg]:rotate-180", 32 | className 33 | )} 34 | {...props} 35 | > 36 | {children} 37 | 38 | 39 | 40 | )) 41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName 42 | 43 | const AccordionContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, children, ...props }, ref) => ( 47 | 52 |
{children}
53 |
54 | )) 55 | 56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName 57 | 58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } 59 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 13 | }, 14 | }, 15 | defaultVariants: { 16 | variant: "default", 17 | }, 18 | }, 19 | ) 20 | 21 | const Alert = React.forwardRef< 22 | HTMLDivElement, 23 | React.HTMLAttributes & VariantProps 24 | >(({ className, variant, ...props }, ref) => ( 25 |
26 | )) 27 | Alert.displayName = "Alert" 28 | 29 | const AlertTitle = React.forwardRef>( 30 | ({ className, ...props }, ref) => ( 31 |
32 | ), 33 | ) 34 | AlertTitle.displayName = "AlertTitle" 35 | 36 | const AlertDescription = React.forwardRef>( 37 | ({ className, ...props }, ref) => ( 38 |
39 | ), 40 | ) 41 | AlertDescription.displayName = "AlertDescription" 42 | 43 | export { Alert, AlertTitle, AlertDescription } 44 | -------------------------------------------------------------------------------- /components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 12 | secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 13 | destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 14 | outline: "text-foreground", 15 | }, 16 | }, 17 | defaultVariants: { 18 | variant: "default", 19 | }, 20 | }, 21 | ) 22 | 23 | export interface BadgeProps extends React.HTMLAttributes, VariantProps {} 24 | 25 | function Badge({ className, variant, ...props }: BadgeProps) { 26 | return
27 | } 28 | 29 | export { Badge, badgeVariants } 30 | -------------------------------------------------------------------------------- /components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { ChevronRight, MoreHorizontal } from "lucide-react" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>