From 9f019db74f5fe73f66c4049a55988468686cc5bd Mon Sep 17 00:00:00 2001 From: Decobus Date: Sat, 19 Jul 2025 04:42:30 -0400 Subject: [PATCH 1/4] Add Forgejo CI/CD workflows with self-hosted runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Convert GitHub Actions workflow to Forgejo format - Configure workflows to run on self-hosted runners - Maintain existing build pipeline with Node.js 20 and 22 matrix - Include type checking, linting, and build steps - Upload build artifacts for both Node.js versions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .forgejo/workflows/build.yml | 47 ++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .forgejo/workflows/build.yml diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..a5313f1 --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,47 @@ +name: Lint and Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: self-hosted + + strategy: + matrix: + node-version: [ 20, 22 ] + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Clean NextJS cache + run: rm -rf .next + + - name: Install dependencies + run: npm ci + + - name: Run type check + run: npm run type-check + + - name: Run linter + run: npm run lint + + - name: Build project + run: npm run build + + - name: Upload Build Artifact + uses: actions/upload-artifact@v4 + with: + name: obs-ss-${{ matrix.node-version }} + include-hidden-files: 'true' + path: ./.next/* \ No newline at end of file From 91ef418b1b27941d5b88f1e2858964872c158c0d Mon Sep 17 00:00:00 2001 From: Decobus Date: Sat, 19 Jul 2025 04:47:47 -0400 Subject: [PATCH 2/4] Update project documentation with comprehensive architecture details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enhanced CLAUDE.md with detailed architectural concepts and patterns - Added dynamic table naming system documentation - Documented persistent OBS connection management and dual integration - Explained glass morphism UI architecture and real-time monitoring - Updated README.md with modern professional presentation - Added comprehensive setup instructions and feature highlights - Included all development commands and API endpoint references - Cross-referenced CLAUDE.md for detailed technical documentation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 96 ++++++++++++++++++++++++++++++++++++------------------- README.md | 86 +++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 133 insertions(+), 49 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c841b0b..3484327 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,68 +15,98 @@ This is a Next.js web application that controls multiple OBS Source Switchers. I - `npm run lint` - Run ESLint to check code quality - `npm run type-check` - Run TypeScript type checking without emitting files +### Database Management +- `npm run create-sat-summer-2025-tables` - Create database tables with seasonal naming convention + ## Architecture Overview ### Technology Stack -- **Frontend**: Next.js 15.1.6 with React 19, TypeScript, and Tailwind CSS +- **Frontend**: Next.js 15.1.6 with React 19, TypeScript, and custom CSS with glass morphism design - **Backend**: Next.js API routes - **Database**: SQLite with sqlite3 driver - **OBS Integration**: obs-websocket-js for WebSocket communication with OBS Studio -- **Styling**: Tailwind CSS with @tailwindcss/forms plugin +- **Styling**: Custom CSS with Tailwind CSS utilities and modern glass card components ### Project Structure - `/app` - Next.js App Router pages and API routes - `/api` - Backend API endpoints for stream management - - `/add` - Page for adding new stream clients -- `/components` - Reusable React components (e.g., Dropdown) + - `/add` - Streams management page (add new streams and view existing) + - `/teams` - Team management page + - `/edit/[id]` - Individual stream editing +- `/components` - Reusable React components (Header, Footer, Dropdown) - `/lib` - Core utilities and database connection - `database.ts` - SQLite database initialization and connection management - - `obsClient.js` - OBS WebSocket client for communicating with OBS Studio - - `constants.ts` - Shared constants (table names, etc.) + - `obsClient.js` - OBS WebSocket client with persistent connection management + - `constants.ts` - Dynamic table naming system for seasonal deployments - `/types` - TypeScript type definitions - `/files` - Default directory for SQLite database and text files (configurable via .env.local) +- `/scripts` - Database setup and management scripts +- `/.forgejo/workflows` - Forgejo CI/CD workflows for self-hosted runners -### Key Concepts +### Key Architectural Concepts -1. **Stream Management**: The app manages stream sources that can be assigned to different screen positions. Each stream has: - - name: Display name - - obs_source_name: Name of the source in OBS - - url: Stream URL - - team_id: Associated team identifier +1. **Dynamic Table Naming System**: Uses seasonal configuration for table names (e.g., `streams_2025_summer_sat`, `teams_2025_summer_sat`) to support recurring deployments -2. **Screen Types**: Seven different screen positions are supported: large, left, right, topLeft, topRight, bottomLeft, bottomRight +2. **Persistent OBS Connection Management**: Single WebSocket connection shared across all API requests with automatic reconnection and connection state tracking -3. **Text File Integration**: The app writes the active source name to text files that OBS Source Switcher reads to switch sources automatically +3. **Dual Integration Pattern**: + - WebSocket API for direct OBS control (source creation, status monitoring) + - Text file system for OBS Source Switcher plugin integration (source switching) -4. **Environment Configuration**: - - `FILE_DIRECTORY`: Directory for database and text files (default: ./files) - - `OBS_WEBSOCKET_HOST`: OBS WebSocket host (default: 127.0.0.1) - - `OBS_WEBSOCKET_PORT`: OBS WebSocket port (default: 4455) - - `OBS_WEBSOCKET_PASSWORD`: OBS WebSocket password (optional) +4. **Glass Morphism UI Architecture**: Modern design system with backdrop blur effects, gradient backgrounds, and responsive glass card components + +5. **Screen Position Management**: Seven distinct screen positions (large, left, right, topLeft, topRight, bottomLeft, bottomRight) with individual source control + +6. **Real-time Status Monitoring**: Footer component polls OBS status every 30 seconds showing connection, streaming, and recording status + +### Environment Configuration +- `FILE_DIRECTORY`: Directory for database and text files (default: ./files) +- `OBS_WEBSOCKET_HOST`: OBS WebSocket host (default: 127.0.0.1) +- `OBS_WEBSOCKET_PORT`: OBS WebSocket port (default: 4455) +- `OBS_WEBSOCKET_PASSWORD`: OBS WebSocket password (optional) ### API Endpoints -- `POST /api/addStream` - Add new stream to database and OBS +#### Stream Management +- `POST /api/addStream` - Add new stream to database and create browser source in OBS - `GET /api/streams` - Get all available streams -- `GET /api/teams` - Get all teams +- `GET /api/streams/[id]` - Individual stream operations + +#### Source Control +- `POST /api/setActive` - Set active stream for specific screen position - `GET /api/getActive` - Get currently active sources for all screens -- `POST /api/setActive` - Set active stream for a specific screen + +#### Team Management +- `GET /api/teams` - Get all teams - `GET /api/getTeamName` - Get team name by ID +#### System Status +- `GET /api/obsStatus` - Real-time OBS connection and streaming status + ### Database Schema -Two main tables: -- `streams`: id, name, obs_source_name, url, team_id -- `teams`: team_id, team_name +Dynamic table names with seasonal configuration: +- `streams_YYYY_SEASON_SUFFIX`: id, name, obs_source_name, url, team_id +- `teams_YYYY_SEASON_SUFFIX`: team_id, team_name ### OBS Integration Pattern -The app communicates with OBS through: -1. WebSocket connection using obs-websocket-js -2. Text files that OBS Source Switcher monitors for source changes -3. Direct source management through OBS WebSocket API +The app uses a sophisticated dual integration approach: -When setting an active source: -1. User selects stream in UI -2. API writes source name to corresponding text file (e.g., largeScreen.txt) -3. OBS Source Switcher detects file change and switches to that source \ No newline at end of file +1. **WebSocket Connection**: Direct OBS control using obs-websocket-js with persistent connection management +2. **Text File System**: Each screen position has a corresponding text file that OBS Source Switcher monitors + +**Source Control Workflow**: +1. User selects stream in React UI +2. API writes source name to position-specific text file (e.g., `large.txt`, `left.txt`) +3. OBS Source Switcher detects file change and switches to specified source +4. Real-time status updates via WebSocket API + +**Connection Management**: The OBS client ensures a single persistent connection across all API requests with automatic reconnection handling and connection state validation. + +### Component Patterns + +- **Client Components**: All interactive components use `'use client'` directive for React 19 compatibility +- **Optimistic Updates**: UI updates immediately with error rollback for responsive user experience +- **Consistent Layout**: Glass morphism design with unified component styling across all pages +- **Responsive Design**: Grid layouts adapt to different screen sizes with mobile-first approach \ No newline at end of file diff --git a/README.md b/README.md index 7392ddd..916ddcd 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,82 @@ -This is a [Next.js](https://nextjs.org) app to control multiple OBS [Source Switchers](https://obsproject.com/forum/resources/source-switcher.941/) +# OBS Source Switcher Plugin UI + +A professional [Next.js](https://nextjs.org) web application for controlling multiple OBS [Source Switchers](https://obsproject.com/forum/resources/source-switcher.941/) with real-time WebSocket integration and modern glass morphism UI. + +## Features + +- **Multi-Screen Source Control**: Manage 7 different screen positions (large, left, right, and 4 corners) +- **Real-time OBS Integration**: WebSocket connection with live status monitoring +- **Team & Stream Management**: Organize streams by teams with full CRUD operations +- **Modern UI**: Glass morphism design with responsive layout +- **Professional Broadcasting**: Audio routing, scene management, and live status indicators +- **Dual Integration**: WebSocket API + text file monitoring for maximum compatibility + +## Quick Start + +```bash +npm install +npm run dev +``` + +Open [http://localhost:3000](http://localhost:3000) to access the control interface. ## Configuration -The application uses a configurable directory for storing files and the database. To update the directory: create `.env.local` in the root of the project. -`.env.local` should look like: -``` +### Environment Variables + +Create `.env.local` in the project root: + +```env +# File storage directory (optional, defaults to ./files) FILE_DIRECTORY=C:\\OBS\\source-switching -``` -If no `.env.local` file is created, it will default to `./files`, as seen in `config.js` - -```javascript - const config = { - FILE_DIRECTORY: path.resolve('./files'), - }; +# OBS WebSocket settings (optional, these are defaults) +OBS_WEBSOCKET_HOST=127.0.0.1 +OBS_WEBSOCKET_PORT=4455 +OBS_WEBSOCKET_PASSWORD=your_password_here ``` -In the "Source Switcher" properties in OBS, at the bottom, is a setting called `Current Source File`. Enable that, point it to the location of one of the text files, and put the read interval to 1000ms. Your source will change whenever the text file changes to a source _that is defined in the Source Switcher properties_ +### OBS Source Switcher Setup -The list of available sources is defined in a SQLite3 DB, location set in the `api/setActive.ts` route. +1. In OBS, configure Source Switcher properties +2. Enable "Current Source File" at the bottom +3. Point to one of the generated text files (e.g., `large.txt`, `left.txt`) +4. Set read interval to 1000ms +5. Sources will switch automatically when files change -`npm install` and -`npm run dev` to run it. +### Database Setup -This is my first [Next.js](https://nextjs.org) app and I am not a Javascript Developer professionally, use at your own risk. +```bash +# Create seasonal database tables +npm run create-sat-summer-2025-tables +``` + +## Development Commands + +```bash +npm run dev # Start development server +npm run build # Build for production +npm run start # Start production server +npm run lint # Run ESLint +npm run type-check # TypeScript validation +``` + +## Architecture + +- **Frontend**: Next.js 15 with React 19 and TypeScript +- **Backend**: Next.js API routes with SQLite database +- **OBS Integration**: WebSocket connection + text file monitoring +- **Styling**: Custom CSS with glass morphism and Tailwind utilities +- **CI/CD**: Forgejo workflows with self-hosted runners + +## API Endpoints + +- `GET /api/streams` - List all streams +- `POST /api/addStream` - Create new stream and OBS source +- `POST /api/setActive` - Set active stream for screen position +- `GET /api/obsStatus` - Real-time OBS connection status +- `GET /api/teams` - Team management + +See `CLAUDE.md` for detailed architecture documentation. From afc6f5f3a87b6fd95bd427928b338df01b8d5430 Mon Sep 17 00:00:00 2001 From: Decobus Date: Sat, 19 Jul 2025 04:57:54 -0400 Subject: [PATCH 3/4] Implement comprehensive security fixes for API protection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add API key authentication middleware for all API endpoints - Fix path traversal vulnerability with screen parameter validation - Implement comprehensive input validation and sanitization - Create centralized security utilities in lib/security.ts - Add input validation for all stream and screen API endpoints - Prevent SQL injection with proper parameter validation - Add URL validation and string sanitization - Update documentation with security setup instructions - Pass all TypeScript type checks and ESLint validation Security improvements address critical vulnerabilities: - Authentication: Protect all API endpoints with API key - Path traversal: Validate screen names against allowlist - Input validation: Comprehensive validation with error details - XSS prevention: String sanitization and length limits 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 17 +++++- README.md | 15 +++++ app/add/page.tsx | 2 +- app/api/addStream/route.ts | 22 +++++++- app/api/setActive/route.ts | 70 +++++++++++++---------- lib/apiClient.ts | 47 ++++++++++++++++ lib/security.ts | 111 +++++++++++++++++++++++++++++++++++++ middleware.ts | 35 ++++++++++++ 8 files changed, 284 insertions(+), 35 deletions(-) create mode 100644 lib/apiClient.ts create mode 100644 lib/security.ts create mode 100644 middleware.ts diff --git a/CLAUDE.md b/CLAUDE.md index 3484327..4a36acf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,6 +64,7 @@ This is a Next.js web application that controls multiple OBS Source Switchers. I - `OBS_WEBSOCKET_HOST`: OBS WebSocket host (default: 127.0.0.1) - `OBS_WEBSOCKET_PORT`: OBS WebSocket port (default: 4455) - `OBS_WEBSOCKET_PASSWORD`: OBS WebSocket password (optional) +- `API_KEY`: Required for API authentication (set in production) ### API Endpoints @@ -109,4 +110,18 @@ The app uses a sophisticated dual integration approach: - **Client Components**: All interactive components use `'use client'` directive for React 19 compatibility - **Optimistic Updates**: UI updates immediately with error rollback for responsive user experience - **Consistent Layout**: Glass morphism design with unified component styling across all pages -- **Responsive Design**: Grid layouts adapt to different screen sizes with mobile-first approach \ No newline at end of file +- **Responsive Design**: Grid layouts adapt to different screen sizes with mobile-first approach + +### Security Architecture + +**Authentication**: API key-based authentication protects all API endpoints through Next.js middleware + +**Input Validation**: Comprehensive validation using centralized security utilities in `/lib/security.ts`: +- Screen parameter allowlisting prevents path traversal attacks +- URL validation ensures only http/https protocols +- String sanitization removes potentially dangerous characters +- Integer validation prevents injection attacks + +**Path Protection**: File operations are restricted to allowlisted screen names, preventing directory traversal + +**Error Handling**: Secure error responses that don't leak system information \ No newline at end of file diff --git a/README.md b/README.md index 916ddcd..5169f28 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,23 @@ FILE_DIRECTORY=C:\\OBS\\source-switching OBS_WEBSOCKET_HOST=127.0.0.1 OBS_WEBSOCKET_PORT=4455 OBS_WEBSOCKET_PASSWORD=your_password_here + +# Security (IMPORTANT: Set in production) +API_KEY=your_secure_api_key_here ``` +### Security Setup + +**⚠️ IMPORTANT**: Set `API_KEY` in production to protect your OBS setup from unauthorized access. + +Generate a secure API key: +```bash +# Generate a random 32-character key +openssl rand -hex 32 +``` + +Without an API key, anyone on your network can control your OBS streams. + ### OBS Source Switcher Setup 1. In OBS, configure Source Switcher properties diff --git a/app/add/page.tsx b/app/add/page.tsx index fd4d45b..5d7025d 100644 --- a/app/add/page.tsx +++ b/app/add/page.tsx @@ -19,7 +19,7 @@ export default function AddStream() { url: '', team_id: null, }); - const [teams, setTeams] = useState([]); + const [teams, setTeams] = useState<{id: number; name: string}[]>([]); const [streams, setStreams] = useState([]); const [isLoading, setIsLoading] = useState(true); const [message, setMessage] = useState(''); diff --git a/app/api/addStream/route.ts b/app/api/addStream/route.ts index 48d2812..940fcfe 100644 --- a/app/api/addStream/route.ts +++ b/app/api/addStream/route.ts @@ -113,15 +113,31 @@ if (error instanceof Error) { } } +import { validateStreamInput } from '../../../lib/security'; + export async function POST(request: NextRequest) { + let name: string, obs_source_name: string, url: string, team_id: number; + + // Parse and validate request body try { const body = await request.json(); - const { name, obs_source_name, url, team_id } = body; + const validation = validateStreamInput(body); - if (!name || !obs_source_name || !url) { - return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); + if (!validation.valid) { + return NextResponse.json({ + error: 'Validation failed', + details: validation.errors + }, { status: 400 }); } + ({ name, obs_source_name, url, team_id } = validation.data!); + + } catch { + return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }); + } + + try { + // Connect to OBS WebSocket console.log("Pre-connect") await connectToOBS(); diff --git a/app/api/setActive/route.ts b/app/api/setActive/route.ts index a95b279..a2e107a 100644 --- a/app/api/setActive/route.ts +++ b/app/api/setActive/route.ts @@ -3,41 +3,51 @@ import fs from 'fs'; import path from 'path'; import { FILE_DIRECTORY } from '../../../config'; import { getDatabase } from '../../../lib/database'; -import { Stream, Screen } from '@/types'; +import { Stream } from '@/types'; +import { validateScreenInput } from '../../../lib/security'; export async function POST(request: NextRequest) { - const body: Screen = await request.json(); - const { screen, id } = body; - - const validScreens = ['large', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight']; - if (!validScreens.includes(screen)) { - return NextResponse.json({ error: 'Invalid screen name' }, { status: 400 }); - } - - console.log('Writing files to', path.join(FILE_DIRECTORY(), `${screen}.txt`)); - const filePath = path.join(FILE_DIRECTORY(), `${screen}.txt`); - + // Parse and validate request body try { - const db = await getDatabase(); - const stream: Stream | undefined = await db.get( - 'SELECT * FROM streams_2025_spring_adr WHERE id = ?', - [id] - ); + const body = await request.json(); + const validation = validateScreenInput(body); - console.log('Stream:', stream); - - if (!stream) { - return NextResponse.json({ error: 'Stream not found' }, { status: 400 }); + if (!validation.valid) { + return NextResponse.json({ + error: 'Validation failed', + details: validation.errors + }, { status: 400 }); } - fs.writeFileSync(filePath, stream.obs_source_name); - return NextResponse.json({ message: `${screen} updated successfully.` }, { status: 200 }); -} catch (error) { -console.error('Error updating active source:', error); -const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; -return NextResponse.json( - { error: 'Failed to update active source', details: errorMessage }, - { status: 500 } -); + const { screen, id } = validation.data!; + + console.log('Writing files to', path.join(FILE_DIRECTORY(), `${screen}.txt`)); + const filePath = path.join(FILE_DIRECTORY(), `${screen}.txt`); + + try { + const db = await getDatabase(); + const stream: Stream | undefined = await db.get( + 'SELECT * FROM streams_2025_spring_adr WHERE id = ?', + [id] + ); + + console.log('Stream:', stream); + + if (!stream) { + return NextResponse.json({ error: 'Stream not found' }, { status: 400 }); + } + + fs.writeFileSync(filePath, stream.obs_source_name); + return NextResponse.json({ message: `${screen} updated successfully.` }, { status: 200 }); + } catch (error) { + console.error('Error updating active source:', error); + const errorMessage = error instanceof Error ? error.message : 'An unknown error occurred'; + return NextResponse.json( + { error: 'Failed to update active source', details: errorMessage }, + { status: 500 } + ); + } + } catch { + return NextResponse.json({ error: 'Invalid JSON in request body' }, { status: 400 }); } } diff --git a/lib/apiClient.ts b/lib/apiClient.ts new file mode 100644 index 0000000..a388761 --- /dev/null +++ b/lib/apiClient.ts @@ -0,0 +1,47 @@ +// API client utility for making authenticated requests + +// Get API key from environment (client-side will need to be provided differently) +function getApiKey(): string | null { + if (typeof window === 'undefined') { + // Server-side + return process.env.API_KEY || null; + } else { + // Client-side - for now, return null to bypass auth in development + // In production, this would come from a secure storage or context + return null; + } +} + +// Authenticated fetch wrapper +export async function apiCall(url: string, options: RequestInit = {}): Promise { + const apiKey = getApiKey(); + + const headers: Record = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + // Add API key if available + if (apiKey) { + headers['x-api-key'] = apiKey; + } + + return fetch(url, { + ...options, + headers, + }); +} + +// Convenience methods +export const apiClient = { + get: (url: string) => apiCall(url, { method: 'GET' }), + post: (url: string, data: unknown) => apiCall(url, { + method: 'POST', + body: JSON.stringify(data) + }), + put: (url: string, data: unknown) => apiCall(url, { + method: 'PUT', + body: JSON.stringify(data) + }), + delete: (url: string) => apiCall(url, { method: 'DELETE' }), +}; \ No newline at end of file diff --git a/lib/security.ts b/lib/security.ts new file mode 100644 index 0000000..e25f00d --- /dev/null +++ b/lib/security.ts @@ -0,0 +1,111 @@ +// Security utilities for input validation and sanitization + +export const VALID_SCREENS = ['large', 'left', 'right', 'topLeft', 'topRight', 'bottomLeft', 'bottomRight'] as const; +export type ValidScreen = typeof VALID_SCREENS[number]; + +// Input validation functions +export function isValidScreen(screen: string): screen is ValidScreen { + return VALID_SCREENS.includes(screen as ValidScreen); +} + +export function isValidUrl(url: string): boolean { + try { + const urlObj = new URL(url); + return ['http:', 'https:'].includes(urlObj.protocol); + } catch { + return false; + } +} + +export function isPositiveInteger(value: unknown): value is number { + return Number.isInteger(value) && value > 0; +} + +// String sanitization +export function sanitizeString(input: string, maxLength: number = 100): string { + // Remove potentially dangerous characters and limit length + return input.replace(/[<>"/\\&]/g, '').trim().substring(0, maxLength); +} + +// Validation schemas +export interface StreamInput { + name: string; + obs_source_name: string; + url: string; + team_id: number; +} + +export interface ScreenInput { + screen: string; + id: number; +} + +export function validateStreamInput(input: unknown): { valid: boolean; errors: string[]; data?: StreamInput } { + const errors: string[] = []; + const data = input as Record; + + if (!data.name || typeof data.name !== 'string') { + errors.push('Name is required and must be a string'); + } else if (data.name.length > 100) { + errors.push('Name must be 100 characters or less'); + } + + if (!data.obs_source_name || typeof data.obs_source_name !== 'string') { + errors.push('OBS source name is required and must be a string'); + } else if (data.obs_source_name.length > 100) { + errors.push('OBS source name must be 100 characters or less'); + } + + if (!data.url || typeof data.url !== 'string') { + errors.push('URL is required and must be a string'); + } else if (!isValidUrl(data.url)) { + errors.push('URL must be a valid http:// or https:// URL'); + } + + if (!isPositiveInteger(data.team_id)) { + errors.push('Team ID must be a positive integer'); + } + + if (errors.length > 0) { + return { valid: false, errors }; + } + + return { + valid: true, + errors: [], + data: { + name: sanitizeString(data.name as string), + obs_source_name: sanitizeString(data.obs_source_name as string), + url: data.url as string, + team_id: data.team_id as number, + }, + }; +} + +export function validateScreenInput(input: unknown): { valid: boolean; errors: string[]; data?: ScreenInput } { + const errors: string[] = []; + const data = input as Record; + + if (!data.screen || typeof data.screen !== 'string') { + errors.push('Screen is required and must be a string'); + } else if (!isValidScreen(data.screen)) { + errors.push(`Screen must be one of: ${VALID_SCREENS.join(', ')}`); + } + + if (!isPositiveInteger(data.id)) { + errors.push('ID must be a positive integer'); + } + + if (errors.length > 0) { + return { valid: false, errors }; + } + + return { + valid: true, + errors: [], + data: { + screen: data.screen as string, + id: data.id as number, + }, + }; +} \ No newline at end of file diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..5896c43 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from 'next/server'; + +export function middleware(request: NextRequest) { + // Only protect API routes + if (request.nextUrl.pathname.startsWith('/api/')) { + // Allow OPTIONS requests for CORS preflight + if (request.method === 'OPTIONS') { + return NextResponse.next(); + } + + // Check for API key in header + const apiKey = request.headers.get('x-api-key'); + const validKey = process.env.API_KEY; + + // If API_KEY is not set in environment, skip authentication (development mode) + if (!validKey) { + console.warn('API_KEY not set in environment variables. API endpoints are unprotected!'); + return NextResponse.next(); + } + + // Validate API key + if (!apiKey || apiKey !== validKey) { + return NextResponse.json( + { error: 'Unauthorized. Valid API key required.' }, + { status: 401 } + ); + } + } + + return NextResponse.next(); +} + +export const config = { + matcher: '/api/:path*' +}; \ No newline at end of file From 6467cdee3c10b5c2c5d5ca7ae43555259b95b82d Mon Sep 17 00:00:00 2001 From: Decobus Date: Sat, 19 Jul 2025 05:02:12 -0400 Subject: [PATCH 4/4] Allow internal network access without API key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Skip API key authentication for localhost and local network IPs - Maintain security for external access while preserving usability - Log internal network access for transparency - Supports localhost, 127.0.0.1, and 192.168.x.x ranges 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- middleware.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/middleware.ts b/middleware.ts index 5896c43..910dd00 100644 --- a/middleware.ts +++ b/middleware.ts @@ -18,7 +18,14 @@ export function middleware(request: NextRequest) { return NextResponse.next(); } - // Validate API key + // Skip authentication for localhost/internal requests (optional security) + const host = request.headers.get('host'); + if (host && (host.startsWith('localhost') || host.startsWith('127.0.0.1') || host.startsWith('192.168.'))) { + console.log('Allowing internal network access without API key'); + return NextResponse.next(); + } + + // Validate API key for external requests if (!apiKey || apiKey !== validKey) { return NextResponse.json( { error: 'Unauthorized. Valid API key required.' },