Welcome to the ultimate Let's Chat documentation. This repository contains the complete source code and infrastructure for a highly scalable, real-time messaging application deeply inspired by WhatsApp. It features a modern, responsive single-page application (SPA) frontend built with Vanilla JavaScript and Vite, backed by a robust RESTful API built on PHP 8.1+ and Laravel 10.
- Executive Summary
- Core Features & Capabilities
- Technology Stack Detailed Breakdown
- Architecture & System Design
- Directory Structure Analysis
- Comprehensive Installation Guide
- Environment Configuration
- Database Schema in Detail
- Comprehensive API Reference
- Frontend Modules & Event Flow
- Backend Architecture
- Real-time Communication Flow (WebRTC & WebSockets)
- Extensive Testing Guide
- Deployment & Hosting
- Security & Privacy Model
- Performance Optimization
- Contributing & Code of Conduct
- Roadmap & Future Scope
- Frequently Asked Questions (FAQ)
- License
Let's Chat goes beyond simple text messaging, offering a complete multimedia experience. From voice notes, image uploads, and file attachments to GIFs and Stickers, the application aims to recreate a premium messaging experience.
Key pillars of the project:
- WhatsApp-like Presence: Real-time "Online" status, "Last seen" timestamps, and message read receipts (Single tick ✓, Double tick ✓✓, Blue tick ✓✓).
- Real-time Communications: Fully functional peer-to-peer Audio and Video calling powered by WebRTC, utilizing a custom PHP WebSocket signaling server.
- Performant UI: Employing optimistic UI updates and lightweight HTTP polling for messaging to avoid heavy WebSocket overhead, while reserving raw TCP WebSockets exclusively for WebRTC call signaling.
- Text Messages: Send and receive with optimistic UI updates (instant local display).
- Voice Messages: Record, send, and playback with duration tracking via Web Audio API. Long-press to record, release to send.
- Image Messages: Attach and preview images inline. Base64 encoding for smooth transport.
- GIF & Sticker Support: Integrated seamlessly with the GIPHY API.
- File Attachments: Share documents, PDFs, and archives with immediate download links.
- Validation: 1000-character limit on the frontend UI, with strict 5000-character backend validation to prevent abuse.
- Message Status Ticks:
- Single ✓: Sent to the server successfully.
- Double ✓✓: Delivered to the recipient's device.
- Blue ✓✓: Read by the recipient (thread opened).
- Online Presence: Green dot for active users. "Last seen X min ago" for offline users.
- Heartbeat System: 30-second background pulse to track real-time user presence.
- Strict Privacy: Conversations are strictly isolated between two participants. Group chats are not currently supported by design.
- WebRTC P2P: Peer-to-peer audio/video calls ensuring end-to-end privacy and zero server bandwidth consumption for media streams.
- Signaling Server: Custom PHP WebSocket server for call negotiation (SDP exchange).
- NAT Traversal: Out-of-the-box ICE/STUN/TURN server support.
- Call States: Comprehensive state machine handling Ringing → Accepted → Ended/Declined.
- Dynamic Theming: Dark / Light theme toggle with
localStoragepersistence. - State Persistence: Conversation caching in
localStoragefor instant sidebar loads upon refresh. - Client-Side Routing: Route-based SPA with hash-free client-side routing (
history.pushState). - Responsive Layout: Fluid flexbox/grid layout featuring a sidebar, active thread, and profile panels.
- Internationalization (i18n): Multi-language / Unicode support including RTL-ready input fields.
- Rich Input: Emoji picker with 50+ built-in emojis and smooth micro-animations.
- Framework: Vanilla JavaScript (ES Modules). We avoided heavy frameworks (React/Vue) to demonstrate deep understanding of DOM manipulation and native browser APIs.
- Build Tool: Vite 5 for blazing-fast HMR and optimized production bundling.
- Styling: Vanilla CSS (Custom Design System with CSS variables).
- Typography: Google Fonts (Plus Jakarta Sans, Sora).
- Framework: Laravel 10 (PHP 8.1+). Provides robust routing, ORM, and dependency injection.
- Authentication: Laravel Sanctum (Token-based, stateless API authentication).
- Database: MySQL 8.0 (using
utf8mb4_unicode_cifor robust emoji and multilingual support).
- WebSockets: Custom raw TCP socket server written in PHP (
stream_socket_server). - Media Transports: WebRTC (RTCPeerConnection, getUserMedia API).
- External APIs: GIPHY API for dynamic media.
graph TD
A[Client Browser / Frontend SPA] -->|HTTP/REST - Polling 1.2s| B(Laravel API:8001)
A -->|WebSocket wss://| C(PHP Signaling Server:8081)
A <-->|WebRTC Peer-to-Peer| A2[Other Client Browser]
B -->|SQL Queries| D[(MySQL Database 8.0)]
C -->|Memory State| C
Design Philosophy:
- Polling-based messaging: A 1.2-second interval handles chat polling. This drastically reduces the persistent connection overhead on the server compared to maintaining thousands of idle WebSockets.
- WebSocket strictly for calls: The signaling server purely handles WebRTC negotiation payloads (SDPs/ICE candidates) where sub-millisecond latency is crucial.
- Optimistic UI: Messages are rendered to the DOM instantly before the server responds, hiding the network latency of the polling architecture.
- Local media storage: Voice and image files are initially captured as base64 data URLs, sent to the server, and saved as physical files to keep database sizes minimal.
chat_app_full/
├── backend/ # Laravel 10 REST API
│ ├── app/
│ │ ├── Http/
│ │ │ ├── Controllers/ # Core business logic
│ │ │ │ ├── AuthController.php # Login, register, logout, /me
│ │ │ │ ├── ChatController.php # Messages, media upload, read receipts
│ │ │ │ ├── CallController.php # WebRTC session state persistence
│ │ │ │ └── UserController.php # Presence heartbeat, user listing
│ │ │ ├── Middleware/ # CORS, auth verification, API rate limits
│ │ │ └── Requests/ # Form request validation logic
│ │ ├── Models/ # Eloquent ORM Models
│ │ │ ├── User.php
│ │ │ ├── Message.php
│ │ │ └── CallSession.php
│ │ └── Providers/
│ ├── config/ # Laravel configuration files
│ ├── realtime/
│ │ └── call_signal_server.php # Raw PHP WebSocket signaling daemon
│ ├── routes/
│ │ └── api.php # Centralized API route definitions
│ ├── storage/ # Uploaded media, logs, framework cache
│ ├── .env.example
│ └── composer.json
│
├── frontend/ # Vanilla JS SPA (Vite)
│ ├── src/
│ │ ├── modules/ # Core logic broken into ES modules
│ │ │ ├── api.js # Fetch wrapper with auto-auth headers
│ │ │ ├── app.js # Page lifecycle and main render logic
│ │ │ ├── auth.js # Authentication form handlers
│ │ │ ├── chat.js # Core chat engine (Polling, rendering)
│ │ │ ├── config.js # Env variables and STUN server config
│ │ │ ├── dom.js # DOM manipulation and XSS prevention
│ │ │ ├── events.js # Global event delegation
│ │ │ ├── router.js # PushState client-side router
│ │ │ ├── state.js # Global state manager & LocalStorage sync
│ │ │ ├── status.js # Presence and read receipt UI logic
│ │ │ └── voice.js # Web Audio API MediaRecorder logic
│ │ └── styles/
│ │ └── app.css # Comprehensive design system (63KB+)
│ ├── index.html # Main SPA entry point
│ ├── vite.config.js # Dev server configuration
│ ├── .env.example
│ └── package.json
│
├── database/
│ └── schema.sql # Complete raw SQL database dump
├── docs/ # Extended internal documentation
├── screenshots/ # UI showcases
└── start-dev.bat # 1-Click Windows Development Launcher
- PHP 8.1 or higher (Extensions:
pdo_mysql,mbstring,openssl) - Composer 2.x
- Node.js 18+ and npm 9+
- MySQL 8.0+ (or MariaDB 10.4+)
- XAMPP/MAMP/WAMP (optional, recommended for local dev)
git clone https://github.com/your-username/chat_app_full.git
cd chat_app_fullCreate your database and import the schema:
CREATE DATABASE IF NOT EXISTS chat_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE chat_app;
SOURCE d:/path/to/chat_app_full/database/schema.sql;cd backend
composer install
cp .env.example .env
php artisan key:generateUpdate your .env file with your MySQL credentials.
cd ../frontend
npm install
cp .env.example .envEnsure your VITE_API_BASE_URL points to your local Laravel server.
If you are on Windows, simply double-click the start-dev.bat file in the root directory. It will launch the Laravel server, the Vite frontend server, and the PHP signaling server in separate command windows.
To run manually:
- API Server:
cd backend && php -S 127.0.0.1:8001 -t public public/index.php - Frontend:
cd frontend && npm run dev - Signaling Server:
cd backend && php realtime/call_signal_server.php
Open http://localhost:5173 in your browser.
| Variable | Description | Example |
|---|---|---|
APP_ENV |
Application environment | local / production |
APP_KEY |
Encryption key | base64:... |
APP_DEBUG |
Enable debug logs | true |
APP_URL |
Base URL of the API | http://127.0.0.1:8001 |
DB_CONNECTION |
Database driver | mysql |
DB_HOST |
Database host IP | 127.0.0.1 |
DB_PORT |
Database port | 3306 |
DB_DATABASE |
Database name | chat_app |
DB_USERNAME |
Database user | root |
DB_PASSWORD |
Database password | secret |
SANCTUM_STATEFUL_DOMAINS |
Domains allowed to access API | localhost:5173,127.0.0.1:5173 |
| Variable | Description | Default |
|---|---|---|
VITE_API_BASE_URL |
Laravel API Endpoint | http://127.0.0.1:8001/api |
VITE_GIPHY_API_KEY |
Optional Giphy Key | your_giphy_key |
VITE_SIGNALING_WS_URL |
WebSocket signaling server | ws://127.0.0.1:8081 |
Handles all user authentication and real-time presence markers.
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
is_online TINYINT(1) DEFAULT 0,
last_seen_at TIMESTAMP NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);The central nervous system of the application, holding all chat data.
CREATE TABLE messages (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
sender_id BIGINT UNSIGNED NOT NULL,
receiver_id BIGINT UNSIGNED NOT NULL,
type VARCHAR(50) DEFAULT 'text', -- text, gif, sticker, image, file, audio, voice
content TEXT NULL,
media_url LONGTEXT NULL,
duration_seconds INT NULL,
status ENUM('sent', 'delivered', 'read') DEFAULT 'sent',
delivered_at TIMESTAMP NULL,
read_at TIMESTAMP NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
FOREIGN KEY (sender_id) REFERENCES users(id),
FOREIGN KEY (receiver_id) REFERENCES users(id)
);Persists WebRTC states allowing recovery and audit trails of calls.
CREATE TABLE call_sessions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
caller_id BIGINT UNSIGNED NOT NULL,
callee_id BIGINT UNSIGNED NOT NULL,
type VARCHAR(50) DEFAULT 'audio', -- audio or video
status VARCHAR(50) DEFAULT 'ringing', -- ringing, accepted, declined, ended
offer_sdp LONGTEXT NULL,
answer_sdp LONGTEXT NULL,
caller_candidates JSON NULL,
callee_candidates JSON NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL,
FOREIGN KEY (caller_id) REFERENCES users(id),
FOREIGN KEY (callee_id) REFERENCES users(id)
);All requests must have the Accept: application/json header. Protected endpoints require the Authorization: Bearer {token} header.
POST /api/register- Payload:
name,email,password,password_confirmation - Returns:
{ user: {...}, token: "..." }
- Payload:
POST /api/login- Payload:
email,password - Returns:
{ user: {...}, token: "..." }
- Payload:
POST /api/logout(Protected)- Revokes current Sanctum token.
GET /api/users- Returns a list of all registered users excluding the current user. Includes
is_onlineflags evaluated againstlast_seen_at.
- Returns a list of all registered users excluding the current user. Includes
POST /api/presence/heartbeat- Hit by the frontend every 30 seconds. Updates the user's
last_seen_attonow().
- Hit by the frontend every 30 seconds. Updates the user's
GET /api/messages/{userId}- Fetches the conversation thread between the authenticated user and
{userId}.
- Fetches the conversation thread between the authenticated user and
POST /api/messages- Payload Example:
{ receiver_id: 2, type: 'text', content: 'Hello World' } - Media Payload:
{ receiver_id: 2, type: 'image', media_url: 'data:image/png;base64,...' } - Returns: The newly created
Messageobject.
- Payload Example:
POST /api/messages/delivered- Marks pending messages in the database as
delivered.
- Marks pending messages in the database as
POST /api/messages/{userId}/read- Upgrades messages from
{userId}toreadstatus.
- Upgrades messages from
POST /api/calls: Initiates a new call session.POST /api/calls/{id}/offer: Submits the SDP offer.POST /api/calls/{id}/answer: Submits the SDP answer.POST /api/calls/{id}/candidate: Transmits ICE candidates.POST /api/calls/{id}/end: Terminates the call session.
A robust wrapper around the native fetch API. It automatically retrieves the token from localStorage and appends it to headers. If a 401 Unauthorized response is intercepted, it clears local state and forces a redirect to the /login route, protecting against expired sessions.
Rather than keeping a persistent WebSocket connection open for chat data, chat.js initiates a setInterval that triggers every 1.2 seconds.
- It calls
GET /api/messages/{activeUserId}. - It compares the response array length against the local DOM.
- If new messages exist, it updates the local state and triggers
POST /api/messages/deliveredautomatically. - If the active tab is focused, it triggers
POST /api/messages/read.
Utilizes navigator.mediaDevices.getUserMedia({ audio: true }).
- Implements
MediaRecorderto capture audio chunks. - A
requestAnimationFrameloop updates the UI recording timer. - On completion, chunks are assembled into a
Blob, converted to Base64 usingFileReader, and sent to the API.
Laravel controllers are kept extremely thin, strictly handling request validation and delegating database queries to Eloquent Models.
ChatController.phpstrictly enforces privacy using awhere(function($q) use ($authId, $targetId) { ... })wrapper to ensure User C cannot query messages belonging to User A and B.
Unlike traditional Laravel sessions, Sanctum provides lightweight Personal Access Tokens. These are issued upon login, stored in the frontend's localStorage, and validated on every request without requiring PHP session files.
Located at realtime/call_signal_server.php, this script binds a socket listener using stream_socket_server('tcp://0.0.0.0:8081').
- Handles raw TCP packet framing to decode WebSocket protocols (RFC 6455).
- Maintains an in-memory array of
$clientsmapped to$user_id. - Instantly proxies JSON payloads between Caller and Callee without touching MySQL.
- Alice wants to call Bob.
- Alice's browser creates a
RTCPeerConnection, generating an Offer SDP. - Alice sends this Offer to the PHP WebSocket server, targeting Bob's ID.
- The PHP Server relays the message instantly to Bob's open WebSocket connection.
- Bob's UI shows a ringing screen. Bob clicks Accept.
- Bob's browser processes the Offer, generates an Answer SDP, and sends it back.
- Both clients exchange network paths (ICE Candidates) via the server.
- Connection Established: P2P Audio/Video stream begins directly between Alice and Bob. Server traffic drops to zero.
- Open two Incognito browsers.
- Register User A in Browser 1, User B in Browser 2.
- User A sends "Hello". Ensure the bubble appears instantly (Optimistic UI) with a Single Tick ✓.
- Once User B's polling fetches it, User A's tick should become a Double Tick ✓✓.
- Register User C in Browser 3. User C must NOT be able to intercept or view User A/B messages.
- Ensure the signaling server is running in the background.
- User A clicks the "Audio Call" icon next to User B's name.
- User B's screen should immediately show an incoming call overlay.
- User B accepts. Grant microphone permissions in both browsers.
- Verify audio transmission using headphones to avoid feedback loops.
- View User B's profile from User A's screen. It should show a Green dot ("Online").
- Close User B's browser completely.
- Wait exactly 45-60 seconds.
- User A's screen should automatically update User B's status to "Last seen 1 minute ago".
Build the frontend for production:
cd frontend
npm run buildThis generates a dist/ directory. Deploy this directory to a static host such as:
- Vercel
- Netlify
- AWS S3 + CloudFront
- Nginx (Serving as static files)
Deploying the PHP API to a VPS (DigitalOcean, AWS EC2, or Heroku):
- Configure your web server (Nginx/Apache) to map the domain to the
backend/public/folder. - Run
composer install --optimize-autoloader --no-dev. - Set
APP_ENV=productionandAPP_DEBUG=falsein.env. - Ensure
storage/andbootstrap/cache/directories have writable permissions (chmod -R 775 storage). - Run
php artisan storage:linkto expose uploaded media to the web.
The PHP WebSocket server must run perpetually. Use Supervisor on Linux:
[program:chat-signaling]
process_name=%(program_name)s
command=php /var/www/chat_app_full/backend/realtime/call_signal_server.php
autostart=true
autorestart=true
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/chat-signaling.log- Media Upload Limits: Base64 strings are aggressively validated. Requests exceeding
500KBfor images or5MBfor voice are instantly rejected to prevent payload bombs. - SQL Injection Prevention: Eloquent ORM is used strictly across all controllers; raw queries are entirely avoided.
- Cross-Site Scripting (XSS): The frontend
dom.jsmodule implements strict character escapingstr.replace(/[&<>'"]/g, ...)before injecting any user-generated content intoinnerHTML. - Stateless Tokens: Auth tokens are stored in memory and
localStorage—there are no cookies vulnerable to CSRF attacks.
- Database Indexes: The
messagestable is optimized with composite indexes onsender_idandreceiver_idto make theWHEREclauses blazing fast during the 1.2s polling cycle. - Optimistic Rendering: The frontend doesn't wait for server confirmation to paint the DOM. This provides an illusion of zero-latency messaging.
- Vite Bundling: All CSS and JS are minified and bundled into single artifacts, heavily reducing HTTP request bottlenecks on initial page load.
We welcome contributions from the open-source community!
- Fork the repository on GitHub.
- Clone your fork locally.
- Create a Feature Branch (
git checkout -b feature/amazing-feature). - Write your code and ensure you adhere to the existing architectural patterns.
- Commit your changes (
git commit -m 'Add some amazing feature'). - Push to the branch (
git push origin feature/amazing-feature). - Open a Pull Request against the
mainbranch.
Please ensure all PRs include relevant updates to documentation and follow the existing coding standard (PSR-12 for PHP, ES6 modular patterns for JS).
While the current application is highly functional, the following features are planned for future iterations:
- [ ] Group Chats: Extending the database schema to support many-to-many message distributions.
- [ ] WebSockets for Text: Transitioning the 1.2s HTTP polling mechanism to full WebSockets for text messaging, completely eliminating HTTP overhead.
- [ ] End-to-End Encryption (E2EE): Implementing the Signal Protocol in JavaScript to encrypt text payloads before they hit the Laravel server.
- [ ] Push Notifications: Integrating Web Push API and Service Workers to notify offline users of new messages.
- [ ] Read Receipts for Voice Notes: Distinct blue microphone icons when a voice note is actually played.
Q: Why use Polling instead of WebSockets for text messages? A: In standard VPS hosting environments, keeping 10,000 idle WebSocket connections open for text chatting consumes massive memory. Short-polling every 1.2s, combined with Optimistic UI, scales incredibly well on standard Apache/Nginx setups without needing a dedicated Redis/Node.js socket infrastructure. We reserve heavy WebSockets only for WebRTC where it is strictly required.
Q: Can I use React or Vue instead of Vanilla JS?
A: Yes. The frontend is completely decoupled from the Laravel API. You can safely delete the frontend folder and build a React SPA pointing to the same /api endpoints.
Q: Why is my WebSocket server crashing?
A: Ensure your server firewall (e.g., UFW or AWS Security Groups) has TCP Port 8081 open to the public. If it crashes, use a process manager like Supervisor to auto-restart it.
This project is fully open-source and available under the MIT License.
You are free to use, modify, distribute, and commercialize this software as long as you include the original copyright and license notice.
Built with ❤️ for the open-source community. Let's Chat!
The following sections contain the complete source code for the application for exhaustive reference.
`json { "name": "chat-app/backend", "type": "project", "description": "Laravel Chat App Backend", "require": { "php": "^8.1", "laravel/framework": "^10.0", "laravel/sanctum": "^3.2" }, "require-dev": { "fakerphp/faker": "^1.9", "phpunit/phpunit": "^10.0" }, "autoload": { "psr-4": { "App\": "app/", "Database\Factories\": "database/factories/", "Database\Seeders\": "database/seeders/" } }, "autoload-dev": { "psr-4": { "Tests\": "tests/" } }, "scripts": { "post-autoload-dump": [ "Illuminate\Foundation\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" ], "post-root-package-install": [ "@php -r "file_exists('.env') || copy('.env.example', '.env');"" ], "post-create-project-cmd": [ "@php artisan key:generate --ansi" ] }, "minimum-stability": "stable", "prefer-stable": true }
`
`php
load(__DIR__.'/Commands'); if (file_exists(base_path('routes/console.php'))) { require base_path('routes/console.php'); } } } ` ### File: backend\app\Exceptions\Handler.php `php reportable(function (Throwable $e) { // }); } } ` ### File: backend\app\Http\Kernel.php `php [ \App\Http\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \App\Http\Middleware\VerifyCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class, ], 'api' => [ 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], ]; protected $middlewareAliases = [ 'auth' => \App\Http\Middleware\Authenticate::class, 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 'can' => \Illuminate\Auth\Middleware\Authorize::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class, 'signed' => \App\Http\Middleware\ValidateSignature::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, ]; } ` ### File: backend\app\Http\Controllers\AuthController.php `php validated(); $payload = [ 'name' => $validated['name'], 'email' => $validated['email'], 'password' => Hash::make($validated['password']), ]; if ($this->supportsPresenceColumns()) { $payload['is_online'] = true; $payload['last_seen_at'] = now(); } $user = User::create($payload); $token = $user->createToken('auth_token')->plainTextToken; return response()->json([ 'message' => 'User registered successfully', 'user' => $user, 'token' => $token, ], 201); } // Login user public function login(LoginRequest $request) { $validated = $request->validated(); $user = User::where('email', $validated['email'])->first(); if (!$user || !Hash::check($validated['password'], $user->password)) { throw ValidationException::withMessages([ 'email' => ['The provided credentials are incorrect.'], ]); } // Revoke old tokens $user->tokens()->delete(); $this->touchPresence($user, true); $token = $user->createToken('auth_token')->plainTextToken; return response()->json([ 'message' => 'Login successful', 'user' => $user, 'token' => $token, ]); } // Logout user public function logout(Request $request) { $this->touchPresence($request->user(), false); $request->user()->currentAccessToken()->delete(); return response()->json([ 'message' => 'Logged out successfully', ]); } // Get authenticated user public function me(Request $request) { $user = $request->user(); $this->touchPresence($user, true); return response()->json($user->fresh()); } private function supportsPresenceColumns(): bool { static $supported = null; if ($supported === null) { $supported = Schema::hasColumn('users', 'is_online') && Schema::hasColumn('users', 'last_seen_at'); } return $supported; } private function touchPresence(User $user, bool $isOnline): void { if (!$this->supportsPresenceColumns()) { return; } $user->forceFill([ 'is_online' => $isOnline, 'last_seen_at' => now(), ])->save(); } } ` ### File: backend\app\Http\Controllers\CallController.php `php caller_id === $userId || $session->callee_id === $userId, 403, 'You are not part of this call session.' ); } private function serializeSession(CallSession $session) { $session->loadMissing([ 'caller:id,name,email', 'callee:id,name,email', ]); return response()->json($session); } public function current(Request $request) { $userId = $request->user()->id; $session = CallSession::with([ 'caller:id,name,email', 'callee:id,name,email', ]) ->whereIn('status', ['ringing', 'accepted']) ->where(function ($query) use ($userId) { $query->where('caller_id', $userId) ->orWhere('callee_id', $userId); }) ->latest('id') ->first(); return response()->json($session); } public function store(StoreCallRequest $request) { $session = CallSession::create([ 'caller_id' => $request->user()->id, 'callee_id' => $request->callee_id, 'type' => $request->type, 'status' => 'ringing', 'caller_candidates' => [], 'callee_candidates' => [], ]); return $this->serializeSession($session); } public function show(Request $request, CallSession $call) { $this->authorizeParticipant($call, $request->user()->id); return $this->serializeSession($call); } public function offer(OfferCallRequest $request, CallSession $call) { abort_unless($call->caller_id === $request->user()->id, 403, 'Only the caller can send the offer.'); $call->offer_sdp = $request->offer_sdp; $call->save(); return $this->serializeSession($call); } public function answer(AnswerCallRequest $request, CallSession $call) { abort_unless($call->callee_id === $request->user()->id, 403, 'Only the callee can answer this call.'); $call->answer_sdp = $request->answer_sdp; $call->status = 'accepted'; $call->started_at = now(); $call->save(); return $this->serializeSession($call); } public function candidate(CandidateCallRequest $request, CallSession $call) { $userId = $request->user()->id; $this->authorizeParticipant($call, $userId); if ($call->caller_id === $userId) { $candidates = $call->caller_candidates ?? []; $candidates[] = $request->candidate; $call->caller_candidates = $candidates; } else { $candidates = $call->callee_candidates ?? []; $candidates[] = $request->candidate; $call->callee_candidates = $candidates; } $call->save(); return $this->serializeSession($call); } public function decline(Request $request, CallSession $call) { abort_unless($call->callee_id === $request->user()->id, 403, 'Only the callee can decline this call.'); $call->status = 'declined'; $call->ended_at = now(); $call->save(); return $this->serializeSession($call); } public function end(Request $request, CallSession $call) { $this->authorizeParticipant($call, $request->user()->id); $call->status = 'ended'; $call->ended_at = now(); $call->save(); return $this->serializeSession($call); } } ` ### File: backend\app\Http\Controllers\ChatController.php `php user(); $supportsPresence = $this->supportsPresenceColumns(); $userSelect = ['id', 'name', 'email', 'created_at']; if ($supportsPresence) { $userSelect[] = 'is_online'; $userSelect[] = 'last_seen_at'; } $users = User::where('id', '!=', $authUser->id) ->select($userSelect) ->orderBy('name', 'asc') ->get(); if ($supportsPresence) { $presenceCutoff = now()->subSeconds(45); $users->transform(function (User $user) use ($presenceCutoff) { $user->is_online = (bool) $user->is_online && $user->last_seen_at !== null && $user->last_seen_at->greaterThan($presenceCutoff); return $user; }); } else { $users->transform(function (User $user) { $user->is_online = false; $user->last_seen_at = null; return $user; }); } $selectedUserId = (int) $request->query('selected_user_id', 0); $selectedUser = $selectedUserId > 0 ? $users->firstWhere('id', $selectedUserId) : null; if (!$selectedUser) { $selectedUser = $users->sortBy([ ['is_online', 'desc'], ['last_seen_at', 'desc'], ['name', 'asc'], ])->first(); } $messages = collect(); if ($selectedUser) { $this->markConversationAsRead($authUser->id, $selectedUser->id); $messages = $this->conversationMessages($authUser->id, $selectedUser->id); } return response()->json([ 'user' => $authUser, 'users' => $users->values(), 'selected_user_id' => $selectedUser?->id, 'messages' => $messages, ]); } // Get all messages between authenticated user and another user public function getMessages(ConversationUserRequest $request, User $user) { $authUserId = $request->user()->id; $otherUserId = $user->id; $this->markConversationAsRead($authUserId, $otherUserId); return response()->json($this->conversationMessages($authUserId, $otherUserId)); } // Send a message public function sendMessage(StoreMessageRequest $request) { $payload = [ 'sender_id' => $request->user()->id, 'receiver_id' => $request->receiver_id, 'type' => $request->input('type', 'text'), 'content' => $request->content, 'media_url' => $this->storeMediaDataUrl($request->media_url), ]; if (Schema::hasColumn('messages', 'duration_seconds')) { $payload['duration_seconds'] = $request->duration_seconds; } $message = Message::create($payload); $message->load(['sender:id,name', 'receiver:id,name']); $message = $this->normalizeMessageMediaUrl($message); return response()->json([ 'message' => 'Message sent successfully', 'data' => $message, ], 201); } public function serveMedia(string $filename): BinaryFileResponse { $safeFilename = basename($filename); $absolutePath = public_path(self::LOCAL_MEDIA_DIRECTORY . DIRECTORY_SEPARATOR . $safeFilename); abort_unless( $safeFilename !== '' && File::exists($absolutePath) && str_starts_with(realpath($absolutePath) ?: '', realpath(public_path(self::LOCAL_MEDIA_DIRECTORY)) ?: ''), 404 ); $fallbackMime = File::mimeType($absolutePath) ?: 'application/octet-stream'; return response()->file($absolutePath, [ 'Content-Type' => $this->guessResponseMimeFromFilename($safeFilename, $fallbackMime), 'Cache-Control' => 'public, max-age=31536000, immutable', 'Accept-Ranges' => 'bytes', ]); } public function clearMessages(ConversationUserRequest $request, User $user) { $authUserId = $request->user()->id; Message::where(function ($query) use ($authUserId, $user) { $query->where('sender_id', $authUserId) ->where('receiver_id', $user->id); }) ->orWhere(function ($query) use ($authUserId, $user) { $query->where('sender_id', $user->id) ->where('receiver_id', $authUserId); }) ->delete(); return response()->json([ 'message' => 'Conversation cleared successfully', ]); } public function markDelivered(Request $request): JsonResponse { if (!$this->supportsMessageStatusColumns()) { return response()->json([ 'message' => 'Message status columns are not available in this database yet.', ]); } $timestamp = now(); Message::where('receiver_id', $request->user()->id) ->whereNull('delivered_at') ->update([ 'delivered_at' => $timestamp, 'updated_at' => $timestamp, ]); return response()->json([ 'message' => 'Pending messages marked as delivered.', ]); } public function markRead(ConversationUserRequest $request, User $user): JsonResponse { $this->markConversationAsRead($request->user()->id, $user->id); return response()->json([ 'message' => 'Conversation marked as read.', ]); } private function markConversationAsRead(int $authUserId, int $otherUserId): void { if (!$this->supportsMessageStatusColumns()) { return; } $timestamp = now(); Message::where('sender_id', $otherUserId) ->where('receiver_id', $authUserId) ->whereNull('read_at') ->update([ 'delivered_at' => $timestamp, 'read_at' => $timestamp, 'updated_at' => $timestamp, ]); } private function conversationMessages(int $authUserId, int $otherUserId) { $messages = Message::where(function ($query) use ($authUserId, $otherUserId) { $query->where(function ($q) use ($authUserId, $otherUserId) { $q->where('sender_id', $authUserId) ->where('receiver_id', $otherUserId); }) ->orWhere(function ($q) use ($authUserId, $otherUserId) { $q->where('sender_id', $otherUserId) ->where('receiver_id', $authUserId); }); }) ->with(['sender:id,name', 'receiver:id,name']) ->orderBy('created_at', 'asc') ->get(); $messages->transform(function (Message $message) { return $this->normalizeMessageMediaUrl($message); }); return $messages; } private function supportsMessageStatusColumns(): bool { static $supported = null; if ($supported === null) { $supported = Schema::hasColumn('messages', 'delivered_at') && Schema::hasColumn('messages', 'read_at'); } return $supported; } private function supportsPresenceColumns(): bool { static $supported = null; if ($supported === null) { $supported = Schema::hasColumn('users', 'is_online') && Schema::hasColumn('users', 'last_seen_at'); } return $supported; } private function normalizeMessageMediaUrl(Message $message): Message { if (!is_string($message->media_url) || trim($message->media_url) === '') { return $message; } $storedPath = $this->normalizeStoredMediaReference($message->media_url, $message->id); if ($storedPath !== $message->media_url) { $message->forceFill([ 'media_url' => $storedPath, ]); $message->saveQuietly(); } $publicUrl = $this->buildPublicMediaUrl($message->media_url); if ($publicUrl !== null) { $message->media_url = $publicUrl; } return $message; } private function storeMediaDataUrl(?string $mediaUrl, ?int $messageId = null): ?string { if (!is_string($mediaUrl) || !str_starts_with($mediaUrl, 'data:')) { return $mediaUrl; } $parsedMedia = $this->parseDataUrl($mediaUrl); if ($parsedMedia === null) { return $mediaUrl; } $directory = public_path(self::LOCAL_MEDIA_DIRECTORY); if (!File::isDirectory($directory)) { File::ensureDirectoryExists($directory); } $extension = $this->guessExtensionFromMime($parsedMedia['mime']); $filename = $messageId ? "message-{$messageId}.{$extension}" : 'message-' . Str::uuid() . ".{$extension}"; $absolutePath = $directory . DIRECTORY_SEPARATOR . $filename; if (!File::exists($absolutePath)) { File::put($absolutePath, $parsedMedia['data']); } return self::LOCAL_MEDIA_DIRECTORY . "/{$filename}"; } private function normalizeStoredMediaReference(?string $mediaUrl, ?int $messageId = null): ?string { if (!is_string($mediaUrl) || trim($mediaUrl) === '') { return $mediaUrl; } if (str_starts_with($mediaUrl, 'data:')) { return $this->storeMediaDataUrl($mediaUrl, $messageId); } $filename = $this->extractStoredMediaFilename($mediaUrl); if ($filename === null) { return $mediaUrl; } return self::LOCAL_MEDIA_DIRECTORY . "/{$filename}"; } private function buildPublicMediaUrl(?string $mediaUrl): ?string { $filename = $this->extractStoredMediaFilename($mediaUrl); if ($filename === null) { return null; } return '/api/media/' . rawurlencode($filename); } private function extractStoredMediaFilename(?string $mediaUrl): ?string { if (!is_string($mediaUrl) || trim($mediaUrl) === '') { return null; } $path = $mediaUrl; if (filter_var($mediaUrl, FILTER_VALIDATE_URL)) { $path = parse_url($mediaUrl, PHP_URL_PATH) ?: ''; } $normalizedPath = str_replace('\\', '/', trim($path)); $marker = '/' . self::LOCAL_MEDIA_DIRECTORY . '/'; if (str_contains($normalizedPath, $marker)) { return basename(substr($normalizedPath, strpos($normalizedPath, $marker) + strlen($marker))); } if (str_starts_with($normalizedPath, self::LOCAL_MEDIA_DIRECTORY . '/')) { return basename(substr($normalizedPath, strlen(self::LOCAL_MEDIA_DIRECTORY . '/'))); } return null; } private function parseDataUrl(string $mediaUrl): ?array { $commaPosition = strpos($mediaUrl, ','); if ($commaPosition === false) { return null; } $metadata = substr($mediaUrl, 5, $commaPosition - 5); $encoded = substr($mediaUrl, $commaPosition + 1); if (!str_contains($metadata, ';base64')) { return null; } $mime = trim(explode(';', $metadata)[0] ?? ''); $decoded = base64_decode($encoded, true); if ($mime === '' || $decoded === false) { return null; } return [ 'mime' => $mime, 'data' => $decoded, ]; } private function guessExtensionFromMime(string $mime): string { $normalizedMime = strtolower(trim(explode(';', $mime)[0] ?? '')); return match ($normalizedMime) { 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', 'image/gif' => 'gif', 'audio/webm' => 'webm', 'audio/mp4', 'audio/x-m4a' => 'm4a', 'audio/mpeg' => 'mp3', 'audio/wav' => 'wav', 'audio/aac', 'audio/mp4a-latm' => 'aac', 'audio/ogg' => 'ogg', 'video/mp4' => 'mp4', 'application/pdf' => 'pdf', default => 'bin', }; } private function guessResponseMimeFromFilename(string $filename, string $fallbackMime): string { return match (strtolower(pathinfo($filename, PATHINFO_EXTENSION))) { 'jpg', 'jpeg' => 'image/jpeg', 'png' => 'image/png', 'gif' => 'image/gif', 'webp' => 'image/webp', 'pdf' => 'application/pdf', 'webm' => 'audio/webm', 'mp3' => 'audio/mpeg', 'wav' => 'audio/wav', 'ogg' => 'audio/ogg', 'm4a', 'mp4' => 'audio/mp4', 'aac' => 'audio/aac', default => $fallbackMime, }; } } ` ### File: backend\app\Http\Controllers\Controller.php `php supportsPresenceColumns(); if ($supportsPresence) { $select[] = 'is_online'; $select[] = 'last_seen_at'; } $users = User::where('id', '!=', $request->user()->id) ->select($select) ->orderBy('name', 'asc') ->get(); if (!$supportsPresence) { $users->transform(function (User $user) { $user->is_online = false; $user->last_seen_at = null; return $user; }); return response()->json($users); } $presenceCutoff = now()->subSeconds(45); $users->transform(function (User $user) use ($presenceCutoff) { $user->is_online = (bool) $user->is_online && $user->last_seen_at !== null && $user->last_seen_at->greaterThan($presenceCutoff); return $user; }); return response()->json($users); } public function heartbeat(Request $request) { if (!$this->supportsPresenceColumns()) { return response()->json([ 'ok' => false, 'message' => 'Presence columns are not available in this database yet.', ]); } $request->user()->forceFill([ 'is_online' => true, 'last_seen_at' => now(), ])->save(); return response()->json([ 'ok' => true, 'last_seen_at' => $request->user()->last_seen_at, ]); } private function supportsPresenceColumns(): bool { static $supported = null; if ($supported === null) { $supported = Schema::hasColumn('users', 'is_online') && Schema::hasColumn('users', 'last_seen_at'); } return $supported; } } ` ### File: backend\app\Http\Middleware\Authenticate.php `php expectsJson()) { return null; } return null; } protected function unauthenticated($request, array $guards) { abort(response()->json([ 'message' => 'Unauthenticated. Please login.' ], 401)); } } ` ### File: backend\app\Http\Middleware\EncryptCookies.php `php check()) { return response()->json([ 'message' => 'Already authenticated.', ], 409); } } return $next($request); } } ` ### File: backend\app\Http\Middleware\TrimStrings.php `php merge([ 'email' => strtolower(trim((string) $this->input('email', ''))), 'password' => (string) $this->input('password', ''), ]); } public function authorize(): bool { return true; } public function rules(): array { return [ 'email' => 'required|email', 'password' => 'required|string', ]; } } ` ### File: backend\app\Http\Requests\Auth\RegisterRequest.php `php merge([ 'name' => trim((string) $this->input('name', '')), 'email' => strtolower(trim((string) $this->input('email', ''))), 'password' => (string) $this->input('password', ''), 'password_confirmation' => (string) $this->input('password_confirmation', ''), ]); } public function authorize(): bool { return true; } public function rules(): array { return [ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users,email', 'password' => 'required|string|min:6|confirmed', ]; } } ` ### File: backend\app\Http\Requests\Calls\AnswerCallRequest.php `php 'required|string', ]; } } ` ### File: backend\app\Http\Requests\Calls\CandidateCallRequest.php `php 'required|array', ]; } } ` ### File: backend\app\Http\Requests\Calls\OfferCallRequest.php `php 'required|string', ]; } } ` ### File: backend\app\Http\Requests\Calls\StoreCallRequest.php `php 'required|exists:users,id|different:' . $this->user()->id, 'type' => 'required|in:voice,video', ]; } } ` ### File: backend\app\Http\Requests\Chat\ConversationUserRequest.php `php merge([ 'conversation_user_id' => $this->route('user')?->id, ]); } public function rules(): array { return [ 'conversation_user_id' => 'required|exists:users,id|different:' . $this->user()->id, ]; } } ` ### File: backend\app\Http\Requests\Chat\StoreMessageRequest.php `php 'required|exists:users,id|different:' . $this->user()->id, 'type' => 'nullable|in:text,gif,sticker,image,file,audio,voice', 'content' => 'required_without:media_url|nullable|string|max:5000', 'media_url' => 'required_without:content|nullable|string|max:500000', 'duration_seconds' => 'nullable|integer|min:1|max:3600', ]; } } ` ### File: backend\app\Models\CallSession.php `php 'array', 'callee_candidates' => 'array', 'started_at' => 'datetime', 'ended_at' => 'datetime', ]; public function caller() { return $this->belongsTo(User::class, 'caller_id'); } public function callee() { return $this->belongsTo(User::class, 'callee_id'); } } ` ### File: backend\app\Models\Message.php `php 'datetime', 'updated_at' => 'datetime', 'delivered_at' => 'datetime', 'read_at' => 'datetime', ]; // Sender relationship public function sender() { return $this->belongsTo(User::class, 'sender_id'); } // Receiver relationship public function receiver() { return $this->belongsTo(User::class, 'receiver_id'); } } ` ### File: backend\app\Models\User.php `php 'datetime', 'password' => 'hashed', 'is_online' => 'boolean', 'last_seen_at' => 'datetime', ]; // Messages sent by this user public function sentMessages() { return $this->hasMany(Message::class, 'sender_id'); } // Messages received by this user public function receivedMessages() { return $this->hasMany(Message::class, 'receiver_id'); } } ` ### File: backend\app\Providers\AppServiceProvider.php `php by($request->user()?->id ?: $request->ip()); }); $this->routes(function () { Route::middleware('api') ->prefix('api') ->group(base_path('routes/api.php')); Route::middleware('web') ->group(base_path('routes/web.php')); }); } } ` ### File: backend\bootstrap\app.php `php singleton( Illuminate\Contracts\Http\Kernel::class, App\Http\Kernel::class ); $app->singleton( Illuminate\Contracts\Console\Kernel::class, App\Console\Kernel::class ); $app->singleton( Illuminate\Contracts\Debug\ExceptionHandler::class, App\Exceptions\Handler::class ); return $app; ` ### File: backend\bootstrap\cache\packages.php `php array ( 'providers' => array ( 0 => 'Laravel\\Sanctum\\SanctumServiceProvider', ), ), 'nesbot/carbon' => array ( 'providers' => array ( 0 => 'Carbon\\Laravel\\ServiceProvider', ), ), 'nunomaduro/termwind' => array ( 'providers' => array ( 0 => 'Termwind\\Laravel\\TermwindServiceProvider', ), ), ); ` ### File: backend\bootstrap\cache\services.php `php array ( 0 => 'Illuminate\\Auth\\AuthServiceProvider', 1 => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', 2 => 'Illuminate\\Bus\\BusServiceProvider', 3 => 'Illuminate\\Cache\\CacheServiceProvider', 4 => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 5 => 'Illuminate\\Cookie\\CookieServiceProvider', 6 => 'Illuminate\\Database\\DatabaseServiceProvider', 7 => 'Illuminate\\Encryption\\EncryptionServiceProvider', 8 => 'Illuminate\\Filesystem\\FilesystemServiceProvider', 9 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider', 10 => 'Illuminate\\Hashing\\HashServiceProvider', 11 => 'Illuminate\\Mail\\MailServiceProvider', 12 => 'Illuminate\\Notifications\\NotificationServiceProvider', 13 => 'Illuminate\\Pagination\\PaginationServiceProvider', 14 => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', 15 => 'Illuminate\\Pipeline\\PipelineServiceProvider', 16 => 'Illuminate\\Queue\\QueueServiceProvider', 17 => 'Illuminate\\Redis\\RedisServiceProvider', 18 => 'Illuminate\\Session\\SessionServiceProvider', 19 => 'Illuminate\\Translation\\TranslationServiceProvider', 20 => 'Illuminate\\Validation\\ValidationServiceProvider', 21 => 'Illuminate\\View\\ViewServiceProvider', 22 => 'Laravel\\Sanctum\\SanctumServiceProvider', 23 => 'Carbon\\Laravel\\ServiceProvider', 24 => 'Termwind\\Laravel\\TermwindServiceProvider', 25 => 'Laravel\\Sanctum\\SanctumServiceProvider', 26 => 'App\\Providers\\AppServiceProvider', 27 => 'App\\Providers\\RouteServiceProvider', ), 'eager' => array ( 0 => 'Illuminate\\Auth\\AuthServiceProvider', 1 => 'Illuminate\\Cookie\\CookieServiceProvider', 2 => 'Illuminate\\Database\\DatabaseServiceProvider', 3 => 'Illuminate\\Encryption\\EncryptionServiceProvider', 4 => 'Illuminate\\Filesystem\\FilesystemServiceProvider', 5 => 'Illuminate\\Foundation\\Providers\\FoundationServiceProvider', 6 => 'Illuminate\\Notifications\\NotificationServiceProvider', 7 => 'Illuminate\\Pagination\\PaginationServiceProvider', 8 => 'Illuminate\\Session\\SessionServiceProvider', 9 => 'Illuminate\\View\\ViewServiceProvider', 10 => 'Laravel\\Sanctum\\SanctumServiceProvider', 11 => 'Carbon\\Laravel\\ServiceProvider', 12 => 'Termwind\\Laravel\\TermwindServiceProvider', 13 => 'Laravel\\Sanctum\\SanctumServiceProvider', 14 => 'App\\Providers\\AppServiceProvider', 15 => 'App\\Providers\\RouteServiceProvider', ), 'deferred' => array ( 'Illuminate\\Broadcasting\\BroadcastManager' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', 'Illuminate\\Contracts\\Broadcasting\\Factory' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', 'Illuminate\\Contracts\\Broadcasting\\Broadcaster' => 'Illuminate\\Broadcasting\\BroadcastServiceProvider', 'Illuminate\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider', 'Illuminate\\Contracts\\Bus\\Dispatcher' => 'Illuminate\\Bus\\BusServiceProvider', 'Illuminate\\Contracts\\Bus\\QueueingDispatcher' => 'Illuminate\\Bus\\BusServiceProvider', 'Illuminate\\Bus\\BatchRepository' => 'Illuminate\\Bus\\BusServiceProvider', 'Illuminate\\Bus\\DatabaseBatchRepository' => 'Illuminate\\Bus\\BusServiceProvider', 'cache' => 'Illuminate\\Cache\\CacheServiceProvider', 'cache.store' => 'Illuminate\\Cache\\CacheServiceProvider', 'cache.psr6' => 'Illuminate\\Cache\\CacheServiceProvider', 'memcached.connector' => 'Illuminate\\Cache\\CacheServiceProvider', 'Illuminate\\Cache\\RateLimiter' => 'Illuminate\\Cache\\CacheServiceProvider', 'Illuminate\\Foundation\\Console\\AboutCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Cache\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Cache\\Console\\ForgetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ClearCompiledCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Auth\\Console\\ClearResetsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ConfigCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ConfigClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ConfigShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\DbCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\PruneCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\ShowCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\WipeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\DownCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\EnvironmentCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\EnvironmentDecryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\EnvironmentEncryptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\EventCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\EventClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\EventListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\KeyGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\OptimizeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\OptimizeClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\PackageDiscoverCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Cache\\Console\\PruneStaleTagsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\ClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\ListFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\FlushFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\ForgetFailedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\ListenCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\MonitorCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\PruneBatchesCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\PruneFailedJobsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\RestartCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\RetryCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\RetryBatchCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\WorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\RouteCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\RouteClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\RouteListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\DumpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Seeds\\SeedCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Console\\Scheduling\\ScheduleFinishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Console\\Scheduling\\ScheduleListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Console\\Scheduling\\ScheduleRunCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Console\\Scheduling\\ScheduleClearCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Console\\Scheduling\\ScheduleTestCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Console\\Scheduling\\ScheduleWorkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Console\\Scheduling\\ScheduleInterruptCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\ShowModelCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\StorageLinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\StorageUnlinkCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\UpCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ViewCacheCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ViewClearCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Cache\\Console\\CacheTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\CastMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ChannelListCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ChannelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ComponentMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ConsoleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Routing\\Console\\ControllerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\DocsCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\EventGenerateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\EventMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ExceptionMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Factories\\FactoryMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\JobMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\LangPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ListenerMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\MailMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Routing\\Console\\MiddlewareMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ModelMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\NotificationMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Notifications\\Console\\NotificationTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ObserverMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\PolicyMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ProviderMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\FailedTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\TableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Queue\\Console\\BatchesTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\RequestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ResourceMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\RuleMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ScopeMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Seeds\\SeederMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Session\\Console\\SessionTableCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ServeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\StubPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\TestMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\VendorPublishCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Foundation\\Console\\ViewMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'migrator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'migration.repository' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'migration.creator' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Migrations\\MigrateCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Migrations\\FreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Migrations\\InstallCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Migrations\\RefreshCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Migrations\\ResetCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Migrations\\RollbackCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Migrations\\StatusCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'Illuminate\\Database\\Console\\Migrations\\MigrateMakeCommand' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'composer' => 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider', 'hash' => 'Illuminate\\Hashing\\HashServiceProvider', 'hash.driver' => 'Illuminate\\Hashing\\HashServiceProvider', 'mail.manager' => 'Illuminate\\Mail\\MailServiceProvider', 'mailer' => 'Illuminate\\Mail\\MailServiceProvider', 'Illuminate\\Mail\\Markdown' => 'Illuminate\\Mail\\MailServiceProvider', 'auth.password' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', 'auth.password.broker' => 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider', 'Illuminate\\Contracts\\Pipeline\\Hub' => 'Illuminate\\Pipeline\\PipelineServiceProvider', 'pipeline' => 'Illuminate\\Pipeline\\PipelineServiceProvider', 'queue' => 'Illuminate\\Queue\\QueueServiceProvider', 'queue.connection' => 'Illuminate\\Queue\\QueueServiceProvider', 'queue.failer' => 'Illuminate\\Queue\\QueueServiceProvider', 'queue.listener' => 'Illuminate\\Queue\\QueueServiceProvider', 'queue.worker' => 'Illuminate\\Queue\\QueueServiceProvider', 'redis' => 'Illuminate\\Redis\\RedisServiceProvider', 'redis.connection' => 'Illuminate\\Redis\\RedisServiceProvider', 'translator' => 'Illuminate\\Translation\\TranslationServiceProvider', 'translation.loader' => 'Illuminate\\Translation\\TranslationServiceProvider', 'validator' => 'Illuminate\\Validation\\ValidationServiceProvider', 'validation.presence' => 'Illuminate\\Validation\\ValidationServiceProvider', 'Illuminate\\Contracts\\Validation\\UncompromisedVerifier' => 'Illuminate\\Validation\\ValidationServiceProvider', ), 'when' => array ( 'Illuminate\\Broadcasting\\BroadcastServiceProvider' => array ( ), 'Illuminate\\Bus\\BusServiceProvider' => array ( ), 'Illuminate\\Cache\\CacheServiceProvider' => array ( ), 'Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider' => array ( ), 'Illuminate\\Hashing\\HashServiceProvider' => array ( ), 'Illuminate\\Mail\\MailServiceProvider' => array ( ), 'Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider' => array ( ), 'Illuminate\\Pipeline\\PipelineServiceProvider' => array ( ), 'Illuminate\\Queue\\QueueServiceProvider' => array ( ), 'Illuminate\\Redis\\RedisServiceProvider' => array ( ), 'Illuminate\\Translation\\TranslationServiceProvider' => array ( ), 'Illuminate\\Validation\\ValidationServiceProvider' => array ( ), ), ); ` ### File: backend\config\app.php `php env('APP_NAME', 'Laravel'), 'env' => env('APP_ENV', 'production'), 'debug' => (bool) env('APP_DEBUG', false), 'url' => env('APP_URL', 'http://localhost'), 'asset_url' => env('ASSET_URL'), 'timezone' => 'UTC', 'locale' => 'en', 'fallback_locale' => 'en', 'faker_locale' => 'en_US', 'cipher' => 'AES-256-CBC', 'key' => env('APP_KEY'), 'maintenance' => [ 'driver' => 'file', ], 'providers' => [ Illuminate\Auth\AuthServiceProvider::class, Illuminate\Broadcasting\BroadcastServiceProvider::class, Illuminate\Bus\BusServiceProvider::class, Illuminate\Cache\CacheServiceProvider::class, Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, Illuminate\Cookie\CookieServiceProvider::class, Illuminate\Database\DatabaseServiceProvider::class, Illuminate\Encryption\EncryptionServiceProvider::class, Illuminate\Filesystem\FilesystemServiceProvider::class, Illuminate\Foundation\Providers\FoundationServiceProvider::class, Illuminate\Hashing\HashServiceProvider::class, Illuminate\Mail\MailServiceProvider::class, Illuminate\Notifications\NotificationServiceProvider::class, Illuminate\Pagination\PaginationServiceProvider::class, Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, Illuminate\Pipeline\PipelineServiceProvider::class, Illuminate\Queue\QueueServiceProvider::class, Illuminate\Redis\RedisServiceProvider::class, Illuminate\Session\SessionServiceProvider::class, Illuminate\Translation\TranslationServiceProvider::class, Illuminate\Validation\ValidationServiceProvider::class, Illuminate\View\ViewServiceProvider::class, Laravel\Sanctum\SanctumServiceProvider::class, App\Providers\AppServiceProvider::class, App\Providers\RouteServiceProvider::class, ], 'aliases' => Illuminate\Support\Facades\Facade::defaultAliases()->merge([ ])->toArray(), ]; ` ### File: backend\config\auth.php `php [ 'guard' => env('AUTH_GUARD', 'web'), 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), ], 'guards' => [ 'web' => [ 'driver' => 'session', 'provider' => 'users', ], 'sanctum' => [ 'driver' => 'sanctum', 'provider' => 'users', ], ], 'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], ], 'passwords' => [ 'users' => [ 'provider' => 'users', 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), 'expire' => 60, 'throttle' => 60, ], ], 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), ]; ` ### File: backend\config\cache.php `php env('CACHE_DRIVER', 'file'), 'stores' => [ 'array' => [ 'driver' => 'array', 'serialize' => false, ], 'file' => [ 'driver' => 'file', 'path' => storage_path('framework/cache/data'), ], ], 'prefix' => env('CACHE_PREFIX', str(env('APP_NAME', 'laravel'))->slug('_').'_cache_'), ]; ` ### File: backend\config\cors.php `php ['api/*', 'sanctum/csrf-cookie'], 'allowed_methods' => ['*'], 'allowed_origins' => ['http://localhost:5173', 'http://127.0.0.1:5173'], 'allowed_origins_patterns' => [], 'allowed_headers' => ['*'], 'exposed_headers' => [], 'max_age' => 0, 'supports_credentials' => true, ]; ` ### File: backend\config\database.php `php env('DB_CONNECTION', 'mysql'), 'connections' => [ 'mysql' => [ 'driver' => 'mysql', 'url' => env('DATABASE_URL'), 'host' => env('DB_HOST', '127.0.0.1'), 'port' => env('DB_PORT', '3306'), 'database' => env('DB_DATABASE', 'forge'), 'username' => env('DB_USERNAME', 'forge'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), 'charset' => 'utf8mb4', 'collation' => 'utf8mb4_unicode_ci', 'prefix' => '', 'prefix_indexes' => true, 'strict' => true, 'engine' => null, 'options' => extension_loaded('pdo_mysql') ? array_filter([ PDO::ATTR_TIMEOUT => (int) env('DB_CONNECT_TIMEOUT', 5), ]) : [], ], ], 'migrations' => 'migrations', 'redis' => [ 'client' => env('REDIS_CLIENT', 'phpredis'), 'options' => [ 'cluster' => env('REDIS_CLUSTER', 'redis'), 'prefix' => env('REDIS_PREFIX', str(env('APP_NAME', 'laravel'))->slug('_').'_database_'), ], 'default' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0'), ], 'cache' => [ 'url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1'), ], ], ]; ` ### File: backend\config\filesystems.php `php env('FILESYSTEM_DISK', 'local'), 'disks' => [ 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), 'throw' => false, ], 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), 'url' => env('APP_URL').'/storage', 'visibility' => 'public', 'throw' => false, ], ], 'links' => [ public_path('storage') => storage_path('app/public'), ], ]; ` ### File: backend\config\logging.php `php env('LOG_CHANNEL', 'stack'), 'deprecations' => [ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), 'trace' => env('LOG_DEPRECATIONS_TRACE', false), ], 'channels' => [ 'stack' => [ 'driver' => 'stack', 'channels' => explode(',', (string) env('LOG_STACK', 'single')), 'ignore_exceptions' => false, ], 'single' => [ 'driver' => 'single', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), ], 'daily' => [ 'driver' => 'daily', 'path' => storage_path('logs/laravel.log'), 'level' => env('LOG_LEVEL', 'debug'), 'days' => 14, ], 'slack' => [ 'driver' => 'slack', 'url' => env('LOG_SLACK_WEBHOOK_URL'), 'username' => 'Laravel Log', 'emoji' => ':boom:', 'level' => env('LOG_LEVEL', 'critical'), ], 'papertrail' => [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), 'handler_with' => [ 'host' => env('PAPERTRAIL_URL'), 'port' => env('PAPERTRAIL_PORT'), ], ], 'stderr' => [ 'driver' => 'monolog', 'level' => env('LOG_LEVEL', 'debug'), 'handler' => StreamHandler::class, 'with' => [ 'stream' => 'php://stderr', ], ], 'syslog' => [ 'driver' => 'syslog', 'level' => env('LOG_LEVEL', 'debug'), ], 'errorlog' => [ 'driver' => 'errorlog', 'level' => env('LOG_LEVEL', 'debug'), ], 'null' => [ 'driver' => 'monolog', 'handler' => NullHandler::class, ], 'emergency' => [ 'path' => storage_path('logs/laravel.log'), ], ], ]; ` ### File: backend\config\queue.php `php env('QUEUE_CONNECTION', 'sync'), 'connections' => [ 'sync' => [ 'driver' => 'sync', ], 'database' => [ 'driver' => 'database', 'table' => 'jobs', 'queue' => 'default', 'retry_after' => 90, 'after_commit' => false, ], ], 'failed' => [ 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 'database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'failed_jobs', ], ]; ` ### File: backend\config\sanctum.php `php explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf( '%s%s', 'localhost,localhost:3000,localhost:5173,127.0.0.1,127.0.0.1:8000,::1', Sanctum::currentApplicationUrlWithPort() ))), 'guard' => ['web'], 'expiration' => null, 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''), 'middleware' => [ 'authenticate_session' => Laravel\Sanctum\Http\Middleware\AuthenticateSession::class, 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class, 'validate_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class, ], ]; ` ### File: backend\config\services.php `php env('SESSION_DRIVER', 'file'), 'lifetime' => env('SESSION_LIFETIME', 120), 'expire_on_close' => false, 'encrypt' => false, 'files' => storage_path('framework/sessions'), 'connection' => env('SESSION_CONNECTION'), 'table' => 'sessions', 'store' => env('SESSION_STORE'), 'lottery' => [2, 100], 'cookie' => env( 'SESSION_COOKIE', str(env('APP_NAME', 'laravel'))->slug('_').'_session' ), 'path' => '/', 'domain' => env('SESSION_DOMAIN'), 'secure' => env('SESSION_SECURE_COOKIE'), 'http_only' => true, 'same_site' => env('SESSION_SAME_SITE', 'lax'), ]; ` ### File: backend\config\view.php `php [ resource_path('views'), ], 'compiled' => env( 'VIEW_COMPILED_PATH', realpath(storage_path('framework/views')) ?: storage_path('framework/views') ), ]; ` ### File: backend\database\migrations\0001_create_users_table.php `php id(); $table->string('name'); $table->string('email')->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('users'); } }; ` ### File: backend\database\migrations\0002_create_messages_table.php `php id(); $table->foreignId('sender_id') ->constrained('users') ->onDelete('cascade'); $table->foreignId('receiver_id') ->constrained('users') ->onDelete('cascade'); $table->text('content'); $table->timestamps(); // Index for faster query performance $table->index(['sender_id', 'receiver_id']); }); } public function down(): void { Schema::dropIfExists('messages'); } }; ` ### File: backend\database\migrations\2026_03_28_000003_add_type_and_media_url_to_messages_table.php `php string('type')->default('text')->after('receiver_id'); $table->text('media_url')->nullable()->after('content'); }); } public function down(): void { Schema::table('messages', function (Blueprint $table) { $table->dropColumn(['type', 'media_url']); }); } }; ` ### File: backend\database\migrations\2026_03_28_000004_create_call_sessions_table.php `php id(); $table->foreignId('caller_id')->constrained('users')->cascadeOnDelete(); $table->foreignId('callee_id')->constrained('users')->cascadeOnDelete(); $table->string('type'); $table->string('status')->default('ringing'); $table->longText('offer_sdp')->nullable(); $table->longText('answer_sdp')->nullable(); $table->json('caller_candidates')->nullable(); $table->json('callee_candidates')->nullable(); $table->timestamp('started_at')->nullable(); $table->timestamp('ended_at')->nullable(); $table->timestamps(); $table->index(['caller_id', 'callee_id', 'status']); }); } public function down(): void { Schema::dropIfExists('call_sessions'); } }; ` ### File: backend\database\migrations\2026_04_02_000001_add_message_status_and_online_fields.php `php enum('status', ['sent', 'delivered', 'read'])->default('sent')->after('media_url'); $table->timestamp('delivered_at')->nullable()->after('status'); $table->timestamp('read_at')->nullable()->after('delivered_at'); }); // Add online status to users table Schema::table('users', function (Blueprint $table) { $table->boolean('is_online')->default(false)->after('password'); $table->timestamp('last_seen_at')->nullable()->after('is_online'); }); } public function down(): void { Schema::table('messages', function (Blueprint $table) { $table->dropColumn(['status', 'delivered_at', 'read_at']); }); Schema::table('users', function (Blueprint $table) { $table->dropColumn(['is_online', 'last_seen_at']); }); } }; ` ### File: backend\database\migrations\2026_04_02_000005_add_presence_to_users_table.php `php boolean('is_online')->default(false)->after('password'); $table->timestamp('last_seen_at')->nullable()->after('is_online'); }); } public function down(): void { Schema::table('users', function (Blueprint $table) { $table->dropColumn(['is_online', 'last_seen_at']); }); } }; ` ### File: backend\database\migrations\2026_04_02_000006_add_message_status_fields.php `php unsignedInteger('duration_seconds')->nullable()->after('media_url'); $table->timestamp('delivered_at')->nullable()->after('duration_seconds'); $table->timestamp('read_at')->nullable()->after('delivered_at'); }); } public function down(): void { Schema::table('messages', function (Blueprint $table) { $table->dropColumn(['duration_seconds', 'delivered_at', 'read_at']); }); } }; ` ### File: backend\database\migrations\2026_05_01_000001_create_personal_access_tokens_table.php `php id(); $table->morphs('tokenable'); $table->string('name'); $table->string('token', 64)->unique(); $table->text('abilities')->nullable(); $table->timestamp('last_used_at')->nullable(); $table->timestamp('expires_at')->nullable(); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('personal_access_tokens'); } }; ` ### File: backend\database\seeders\DatabaseSeeder.php `php 'Alice Johnson', 'email' => 'alice@example.com'], ['name' => 'Bob Smith', 'email' => 'bob@example.com'], ['name' => 'Charlie Brown', 'email' => 'charlie@example.com'], ['name' => 'Diana Prince', 'email' => 'diana@example.com'], ]; foreach ($users as $userData) { User::firstOrCreate( ['email' => $userData['email']], [ 'name' => $userData['name'], 'password' => Hash::make('password123'), ] ); } } } ` ### File: backend\public\index.php `php make(Kernel::class); $response = $kernel->handle( $request = Request::capture() )->send(); $kernel->terminate($request, $response); ` ### File: backend\realtime\call_signal_server.php `php make(Kernel::class)->bootstrap(); const DEFAULT_SIGNAL_HOST = '127.0.0.1'; const DEFAULT_SIGNAL_PORT = 8081; $host = $argv[1] ?? DEFAULT_SIGNAL_HOST; $port = (int) ($argv[2] ?? DEFAULT_SIGNAL_PORT); $server = stream_socket_server( sprintf('tcp://%s:%d', $host, $port), $errorCode, $errorMessage ); if ($server === false) { fwrite(STDERR, sprintf("Unable to start signaling server: [%d] %s\n", $errorCode, $errorMessage)); exit(1); } stream_set_blocking($server, false); $clients = []; $userSockets = []; fwrite(STDOUT, sprintf("Call signaling WebSocket listening on ws://%s:%d\n", $host, $port)); while (true) { $readSockets = [$server]; foreach ($clients as $client) { $readSockets[] = $client['socket']; } $writeSockets = null; $exceptSockets = null; if (@stream_select($readSockets, $writeSockets, $exceptSockets, null) === false) { continue; } foreach ($readSockets as $socket) { if ($socket === $server) { $connection = @stream_socket_accept($server, 0); if ($connection === false) { continue; } stream_set_blocking($connection, false); $clients[(int) $connection] = [ 'socket' => $connection, 'buffer' => '', 'handshake_complete' => false, 'user_id' => null, ]; continue; } $clientId = (int) $socket; $chunk = @fread($socket, 8192); if ($chunk === '' || $chunk === false) { if (feof($socket)) { disconnectClient($clientId, $clients, $userSockets); } continue; } $clients[$clientId]['buffer'] .= $chunk; if ($clients[$clientId]['handshake_complete'] === false) { if (str_contains($clients[$clientId]['buffer'], "\r\n\r\n")) { completeHandshake($clients[$clientId]); } continue; } processClientFrames($clientId, $clients, $userSockets); } } function completeHandshake(array &$client): void { if (!preg_match("/Sec-WebSocket-Key: (.*)\r\n/i", $client['buffer'], $matches)) { fclose($client['socket']); return; } $key = trim($matches[1]); $acceptKey = base64_encode( sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true) ); $response = implode("\r\n", [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' . $acceptKey, '', '', ]); fwrite($client['socket'], $response); $client['handshake_complete'] = true; $client['buffer'] = ''; } function processClientFrames(int $clientId, array &$clients, array &$userSockets): void { while (true) { $frame = extractFrame($clients[$clientId]['buffer']); if ($frame === null) { return; } if ($frame['opcode'] === 0x8) { disconnectClient($clientId, $clients, $userSockets); return; } if ($frame['opcode'] === 0x9) { sendFrame($clients[$clientId]['socket'], $frame['payload'], 0xA); continue; } if ($frame['opcode'] !== 0x1) { continue; } $payload = json_decode($frame['payload'], true); if (!is_array($payload)) { sendJson($clients[$clientId]['socket'], [ 'type' => 'error', 'message' => 'Invalid signaling payload.', ]); continue; } handleSignalMessage($clientId, $payload, $clients, $userSockets); } } function handleSignalMessage(int $clientId, array $payload, array &$clients, array &$userSockets): void { $type = $payload['type'] ?? ''; if ($type === 'auth') { $token = is_string($payload['token'] ?? null) ? trim($payload['token']) : ''; $user = authenticateToken($token); if (!$user) { sendJson($clients[$clientId]['socket'], [ 'type' => 'auth.error', 'message' => 'Invalid token.', ]); disconnectClient($clientId, $clients, $userSockets); return; } $clients[$clientId]['user_id'] = $user['id']; $userSockets[$user['id']][$clientId] = true; sendJson($clients[$clientId]['socket'], [ 'type' => 'auth.ok', 'user' => $user, ]); return; } if (!$clients[$clientId]['user_id']) { sendJson($clients[$clientId]['socket'], [ 'type' => 'auth.error', 'message' => 'Authenticate before signaling.', ]); disconnectClient($clientId, $clients, $userSockets); return; } $toUserId = (int) ($payload['toUserId'] ?? 0); if ($toUserId < 1 || empty($userSockets[$toUserId])) { return; } $relayPayload = $payload; unset($relayPayload['toUserId'], $relayPayload['token']); $relayPayload['fromUserId'] = $clients[$clientId]['user_id']; $relayPayload['serverTime'] = now()->toIso8601String(); foreach (array_keys($userSockets[$toUserId]) as $targetClientId) { if (!isset($clients[$targetClientId])) { continue; } sendJson($clients[$targetClientId]['socket'], $relayPayload); } } function authenticateToken(string $plainTextToken): ?array { if ($plainTextToken === '') { return null; } $token = PersonalAccessToken::findToken($plainTextToken); if (!$token || !$token->tokenable) { return null; } $user = $token->tokenable->fresh(['id', 'name', 'email']); if (!$user) { return null; } return [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, ]; } function extractFrame(string &$buffer): ?array { $bufferLength = strlen($buffer); if ($bufferLength < 2) { return null; } $firstByte = ord($buffer[0]); $secondByte = ord($buffer[1]); $opcode = $firstByte & 0x0F; $masked = ($secondByte & 0x80) === 0x80; $payloadLength = $secondByte & 0x7F; $offset = 2; if ($payloadLength === 126) { if ($bufferLength < 4) { return null; } $payloadLength = unpack('n', substr($buffer, 2, 2))[1]; $offset = 4; } elseif ($payloadLength === 127) { if ($bufferLength < 10) { return null; } $extended = unpack('N2', substr($buffer, 2, 8)); $payloadLength = ($extended[1] << 32) | $extended[2]; $offset = 10; } $maskKey = ''; if ($masked) { if ($bufferLength < $offset + 4) { return null; } $maskKey = substr($buffer, $offset, 4); $offset += 4; } if ($bufferLength < $offset + $payloadLength) { return null; } $payload = substr($buffer, $offset, $payloadLength); $buffer = substr($buffer, $offset + $payloadLength); if ($masked) { $payload = unmaskPayload($payload, $maskKey); } return [ 'opcode' => $opcode, 'payload' => $payload, ]; } function unmaskPayload(string $payload, string $maskKey): string { $decoded = ''; $payloadLength = strlen($payload); for ($index = 0; $index < $payloadLength; $index++) { $decoded .= $payload[$index] ^ $maskKey[$index % 4]; } return $decoded; } function sendJson($socket, array $payload): void { sendFrame($socket, json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); } function sendFrame($socket, string $payload, int $opcode = 0x1): void { $payloadLength = strlen($payload); $frame = chr(0x80 | ($opcode & 0x0F)); if ($payloadLength < 126) { $frame .= chr($payloadLength); } elseif ($payloadLength <= 65535) { $frame .= chr(126) . pack('n', $payloadLength); } else { $frame .= chr(127) . pack('NN', 0, $payloadLength); } $frame .= $payload; @fwrite($socket, $frame); } function disconnectClient(int $clientId, array &$clients, array &$userSockets): void { if (!isset($clients[$clientId])) { return; } $userId = $clients[$clientId]['user_id']; if ($userId && isset($userSockets[$userId][$clientId])) { unset($userSockets[$userId][$clientId]); if (empty($userSockets[$userId])) { unset($userSockets[$userId]); } } @fclose($clients[$clientId]['socket']); unset($clients[$clientId]); } ` ### File: backend\routes\api.php `php where('filename', '.*'); // Protected Routes (Sanctum auth required) Route::middleware('auth:sanctum')->group(function () { // Auth Route::post('/logout', [AuthController::class, 'logout']); Route::get('/me', [AuthController::class, 'me']); // Users Route::get('/users', [UserController::class, 'index']); Route::post('/presence/heartbeat', [UserController::class, 'heartbeat']); // Chat Route::get('/chat/bootstrap', [ChatController::class, 'bootstrap']); Route::get('/messages/{user}', [ChatController::class, 'getMessages']); Route::post('/messages', [ChatController::class, 'sendMessage']); Route::post('/send-message', [ChatController::class, 'sendMessage']); Route::post('/messages/delivered', [ChatController::class, 'markDelivered']); Route::post('/messages/{user}/read', [ChatController::class, 'markRead']); Route::delete('/messages/{user}', [ChatController::class, 'clearMessages']); // Calls Route::get('/calls/current', [CallController::class, 'current']); Route::post('/calls', [CallController::class, 'store']); Route::get('/calls/{call}', [CallController::class, 'show']); Route::post('/calls/{call}/offer', [CallController::class, 'offer']); Route::post('/calls/{call}/answer', [CallController::class, 'answer']); Route::post('/calls/{call}/candidate', [CallController::class, 'candidate']); Route::post('/calls/{call}/decline', [CallController::class, 'decline']); Route::post('/calls/{call}/end', [CallController::class, 'end']); }); ` ### File: backend\routes\console.php `php comment('Backend is booting normally.'); })->purpose('Confirm Artisan commands are loaded'); ` ### File: backend\routes\web.php `php away(frontendUrl('/')); }); Route::get('/login', fn () => redirect()->away(frontendUrl('/login'))); Route::get('/register', fn () => redirect()->away(frontendUrl('/register'))); Route::get('/chat', fn () => redirect()->away(frontendUrl('/chat'))); ` ### File: database\schema.sql `sql -- Chat App Database Schema -- Run this if you prefer manual DB setup instead of Laravel migrations CREATE DATABASE IF NOT EXISTS chat_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE chat_app; -- Users Table CREATE TABLE IF NOT EXISTS users ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE, email_verified_at TIMESTAMP NULL DEFAULT NULL, password VARCHAR(255) NOT NULL, is_online TINYINT(1) NOT NULL DEFAULT 0, last_seen_at TIMESTAMP NULL DEFAULT NULL, remember_token VARCHAR(100) NULL, created_at TIMESTAMP NULL DEFAULT NULL, updated_at TIMESTAMP NULL DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Messages Table CREATE TABLE IF NOT EXISTS messages ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, sender_id BIGINT UNSIGNED NOT NULL, receiver_id BIGINT UNSIGNED NOT NULL, type VARCHAR(255) NOT NULL DEFAULT 'text', content TEXT NOT NULL, media_url LONGTEXT NULL, duration_seconds INT UNSIGNED NULL, delivered_at TIMESTAMP NULL DEFAULT NULL, read_at TIMESTAMP NULL DEFAULT NULL, created_at TIMESTAMP NULL DEFAULT NULL, updated_at TIMESTAMP NULL DEFAULT NULL, CONSTRAINT fk_sender FOREIGN KEY (sender_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_receiver FOREIGN KEY (receiver_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_conversation (sender_id, receiver_id), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Call Sessions Table CREATE TABLE IF NOT EXISTS call_sessions ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, caller_id BIGINT UNSIGNED NOT NULL, callee_id BIGINT UNSIGNED NOT NULL, type VARCHAR(255) NOT NULL, status VARCHAR(255) NOT NULL DEFAULT 'ringing', offer_sdp LONGTEXT NULL, answer_sdp LONGTEXT NULL, caller_candidates JSON NULL, callee_candidates JSON NULL, started_at TIMESTAMP NULL DEFAULT NULL, ended_at TIMESTAMP NULL DEFAULT NULL, created_at TIMESTAMP NULL DEFAULT NULL, updated_at TIMESTAMP NULL DEFAULT NULL, CONSTRAINT fk_call_caller FOREIGN KEY (caller_id) REFERENCES users(id) ON DELETE CASCADE, CONSTRAINT fk_call_callee FOREIGN KEY (callee_id) REFERENCES users(id) ON DELETE CASCADE, INDEX idx_call_status (caller_id, callee_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- Personal Access Tokens (Laravel Sanctum) CREATE TABLE IF NOT EXISTS personal_access_tokens ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, tokenable_type VARCHAR(255) NOT NULL, tokenable_id BIGINT UNSIGNED NOT NULL, name VARCHAR(255) NOT NULL, token VARCHAR(64) NOT NULL UNIQUE, abilities TEXT NULL, last_used_at TIMESTAMP NULL DEFAULT NULL, expires_at TIMESTAMP NULL DEFAULT NULL, created_at TIMESTAMP NULL DEFAULT NULL, updated_at TIMESTAMP NULL DEFAULT NULL, INDEX idx_tokenable (tokenable_type, tokenable_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ` ### File: frontend\package-lock.json `json { "name": "chat-app-frontend", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chat-app-frontend", "version": "1.0.0", "devDependencies": { "autoprefixer": "^10.4.16", "postcss": "^8.4.31", "tailwindcss": "^3.3.5", "vite": "^5.0.0" } }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", "dev": true, "license": "MIT", "engines": { "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/android-arm": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/android-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/android-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/darwin-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/freebsd-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/freebsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-arm": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-ia32": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-loong64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-mips64el": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-riscv64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-s390x": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/linux-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/netbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/openbsd-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/sunos-x64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { "node": ">=12" } }, "node_modules/@esbuild/win32-arm64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",