Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,7 @@ AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false

VITE_APP_NAME="${APP_NAME}"

# External API Configuration
API_URL=http://localhost:8007/api/v1
API_TIMEOUT=30
28 changes: 23 additions & 5 deletions API_PROXY_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,34 @@ Special endpoint for handling file uploads with multipart form data.

## Frontend Usage

The `api-client.js` is automatically configured to use the proxy:
The `api-client.js` is automatically configured to use the proxy for all API requests:

```javascript
// Before: Direct external API calls
this.baseUrl = 'http://telegram.localhost:8009/api/v1';

// After: Local proxy calls
// All requests go through the local proxy
this.baseUrl = '/api/proxy';

// The external API URL is read from meta tag set by PHP
// This is used for direct health checks and testing connections
this.externalApiUrl = this.getApiUrlFromMeta();
window.API_URL = this.externalApiUrl;
```

### Configuration in .env

```env
# External API URL (used by Laravel and exposed to frontend via meta tag)
API_URL=http://localhost:8007/api/v1
API_TIMEOUT=30
```

The API URL is injected into the HTML head as a meta tag by Laravel:

```html
<meta name="api-url" content="{{ config('api.url') }}">
```

This ensures the API endpoint is configured in one place (`.env`) and properly propagated to both backend (Laravel) and frontend (JavaScript).

## Security Features

- **Authentication**: All proxy routes require `auth` and `verified` middleware
Expand Down
33 changes: 18 additions & 15 deletions FRONTEND_ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,31 +70,34 @@ const shifts = await window.apiClient.getCurrentShifts();

### 2. Configuration

Set the API base URL by defining `window.API_BASE_URL` before loading the API client:
The API URL is configured via environment variable in `.env`:

```html
<script>
window.API_BASE_URL = 'https://your-api-server.com/api/v1';
</script>
<script src="{{ asset('js/app.js') }}"></script>
```env
# External API URL (used by Laravel and exposed to frontend)
API_URL=http://localhost:8007/api/v1
API_TIMEOUT=30
```

Or configure it in your `.env`:
The API URL is passed from Laravel to JavaScript via a meta tag in the HTML head:

```env
VITE_API_BASE_URL=https://your-api-server.com/api/v1
```html
<meta name="api-url" content="{{ config('api.url') }}">
```

Then in your `vite.config.js`:
The API client reads this meta tag on initialization:

```javascript
export default defineConfig({
define: {
'window.API_BASE_URL': JSON.stringify(env.VITE_API_BASE_URL || 'http://localhost:8000/api/v1')
getApiUrlFromMeta() {
const metaTag = document.querySelector('meta[name="api-url"]');
if (metaTag) {
return metaTag.getAttribute('content');
}
});
return 'http://localhost:8007/api/v1'; // Fallback
}
```

The API client automatically exposes this URL as `window.API_URL` for use in views and inline scripts.

### 3. View Implementation Pattern

All views use Alpine.js to fetch and display data dynamically. Example pattern:
Expand Down Expand Up @@ -224,7 +227,7 @@ npm run dev
Set the Telegram Bot API URL in your environment:

```env
VITE_API_BASE_URL=https://your-telegram-bot-api.com/api/v1
API_URL=https://your-telegram-bot-api.com/api/v1
```

### 3. Test API Connectivity
Expand Down
46 changes: 25 additions & 21 deletions app/Http/Controllers/ApiProxyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Http;
use App\Models\Setting;

class ApiProxyController extends Controller
{
Expand All @@ -15,38 +14,46 @@ class ApiProxyController extends Controller
public function proxy(Request $request, string $endpoint): JsonResponse
{
try {
// Get user's API settings
$apiUrl = Setting::getValue('api_url', 'http://host.docker.internal:8007/api/v1');
$apiToken = Setting::getValue('auth_token');
// Get API URL from configuration
$apiUrl = config('api.url');
Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proxy now points to localhost from inside Docker, breaking all external API calls.

Prompt for AI agents
Address the following comment on app/Http/Controllers/ApiProxyController.php at line 18:

<comment>Proxy now points to localhost from inside Docker, breaking all external API calls.</comment>

<file context>
@@ -15,38 +14,46 @@ class ApiProxyController extends Controller
-            $apiUrl = Setting::getValue(&#39;api_url&#39;, &#39;http://host.docker.internal:8007/api/v1&#39;);
-            $apiToken = Setting::getValue(&#39;auth_token&#39;);
+            // Get API URL from configuration
+            $apiUrl = config(&#39;api.url&#39;);
 
-            if (!$apiToken) {
</file context>
Fix with Cubic

Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proxy now hits container-localhost and can't reach host API

Prompt for AI agents
Address the following comment on app/Http/Controllers/ApiProxyController.php at line 18:

<comment>Proxy now hits container-localhost and can&#39;t reach host API</comment>

<file context>
@@ -15,38 +14,46 @@ class ApiProxyController extends Controller
-            $apiUrl = Setting::getValue(&#39;api_url&#39;, &#39;http://host.docker.internal:8007/api/v1&#39;);
-            $apiToken = Setting::getValue(&#39;auth_token&#39;);
+            // Get API URL from configuration
+            $apiUrl = config(&#39;api.url&#39;);
 
-            if (!$apiToken) {
</file context>
Fix with Cubic


if (!$apiToken) {
// Get token from session
$apiToken = $request->session()->get('api_token');

// Define endpoints that don't require authentication
$publicEndpoints = ['register', 'session'];
$isPublicEndpoint = in_array(trim($endpoint, '/'), $publicEndpoints);

// Check authentication for protected endpoints
if (!$isPublicEndpoint && !$apiToken) {
return response()->json([
'error' => 'API token not configured',
'message' => 'Please configure your Telegram Bot API token in settings'
'error' => 'Authentication required',
'message' => 'Please login to access this resource'
], 401);
}

// Replace localhost with host.docker.internal for Docker environment
$apiUrl = str_replace('http://localhost:', 'http://host.docker.internal:', $apiUrl);

// Build the external URL
$externalUrl = rtrim($apiUrl, '/') . '/' . ltrim($endpoint, '/');

// Prepare request headers
$headers = [
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Authorization' => "Bearer {$apiToken}",
];

// Add authorization header only if we have a token
if ($apiToken) {
$headers['Authorization'] = "Bearer {$apiToken}";
}

// Add any custom headers from the original request
if ($request->header('X-Requested-With')) {
$headers['X-Requested-With'] = $request->header('X-Requested-With');
}

// Make the external request
$response = Http::withHeaders($headers)
->timeout(30)
->timeout(config('api.timeout'))
->send(
$request->method(),
$externalUrl,
Expand All @@ -67,7 +74,6 @@ public function proxy(Request $request, string $endpoint): JsonResponse
\Log::error('API Proxy Error: ' . $e->getMessage(), [
'endpoint' => $endpoint,
'method' => $request->method(),
'user_id' => auth()->id(),
]);

return response()->json([
Expand All @@ -84,19 +90,18 @@ public function proxy(Request $request, string $endpoint): JsonResponse
public function proxyUpload(Request $request, string $endpoint): JsonResponse
{
try {
// Get user's API settings
$apiUrl = Setting::getValue('api_url', 'http://host.docker.internal:8007/api/v1');
$apiToken = Setting::getValue('auth_token');
// Get API URL from configuration
$apiUrl = config('api.url');

// Get token from session
$apiToken = $request->session()->get('api_token');

if (!$apiToken) {
return response()->json([
'error' => 'API token not configured'
'error' => 'Authentication required'
], 401);
}

// Replace localhost with host.docker.internal for Docker environment
$apiUrl = str_replace('http://localhost:', 'http://host.docker.internal:', $apiUrl);

$externalUrl = rtrim($apiUrl, '/') . '/' . ltrim($endpoint, '/');

// Prepare headers
Expand Down Expand Up @@ -142,7 +147,6 @@ public function proxyUpload(Request $request, string $endpoint): JsonResponse
} catch (\Exception $e) {
\Log::error('API Proxy Upload Error: ' . $e->getMessage(), [
'endpoint' => $endpoint,
'user_id' => auth()->id(),
]);

return response()->json([
Expand Down
34 changes: 0 additions & 34 deletions app/Http/Controllers/Auth/ConfirmationController.php

This file was deleted.

61 changes: 44 additions & 17 deletions app/Http/Controllers/Auth/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Illuminate\Auth\Events\Lockout;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
Expand All @@ -19,36 +19,63 @@ public function create(): View
return view('auth.login');
}

public function store(Request $request): RedirectResponse
public function store(Request $request): RedirectResponse|\Illuminate\Http\JsonResponse
{
// Frontend has already called the API and is just storing token/user in session
$request->validate([
'email' => ['required', 'string', 'email'],
'password' => ['required', 'string'],
'token' => ['required', 'string'],
'user' => ['required', 'array'],
]);

$this->ensureIsNotRateLimited($request);

if (! Auth::attempt($request->only('email', 'password'), $request->boolean('remember'))) {
RateLimiter::hit($this->throttleKey($request));
try {
// Store authentication data in session
$request->session()->regenerate();
$request->session()->put('api_token', $request->input('token'));
$request->session()->put('user', $request->input('user'));
$request->session()->put('authenticated', true);
Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store() marks the session as authenticated solely from client-provided data, allowing anyone to POST arbitrary token/user values and bypass authentication. Please verify the token (e.g., by calling the external API) before trusting it.

Prompt for AI agents
Address the following comment on app/Http/Controllers/Auth/LoginController.php at line 35:

<comment>`store()` marks the session as authenticated solely from client-provided data, allowing anyone to POST arbitrary `token`/`user` values and bypass authentication. Please verify the token (e.g., by calling the external API) before trusting it.</comment>

<file context>
@@ -19,36 +19,63 @@ public function create(): View
+            $request-&gt;session()-&gt;regenerate();
+            $request-&gt;session()-&gt;put(&#39;api_token&#39;, $request-&gt;input(&#39;token&#39;));
+            $request-&gt;session()-&gt;put(&#39;user&#39;, $request-&gt;input(&#39;user&#39;));
+            $request-&gt;session()-&gt;put(&#39;authenticated&#39;, true);
+
+            if ($request-&gt;expectsJson()) {
</file context>
Fix with Cubic

Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The login handler trusts the client-provided token and user payload and immediately sets the session as authenticated. Because api.auth middleware only checks for these session values, a malicious request can POST arbitrary data to /login and bypass authentication entirely. Please verify the token server-side (e.g., by validating it with the external API) before marking the session as authenticated.

Prompt for AI agents
Address the following comment on app/Http/Controllers/Auth/LoginController.php at line 35:

<comment>The login handler trusts the client-provided token and user payload and immediately sets the session as authenticated. Because api.auth middleware only checks for these session values, a malicious request can POST arbitrary data to /login and bypass authentication entirely. Please verify the token server-side (e.g., by validating it with the external API) before marking the session as authenticated.</comment>

<file context>
@@ -19,36 +19,63 @@ public function create(): View
+            $request-&gt;session()-&gt;regenerate();
+            $request-&gt;session()-&gt;put(&#39;api_token&#39;, $request-&gt;input(&#39;token&#39;));
+            $request-&gt;session()-&gt;put(&#39;user&#39;, $request-&gt;input(&#39;user&#39;));
+            $request-&gt;session()-&gt;put(&#39;authenticated&#39;, true);
+
+            if ($request-&gt;expectsJson()) {
</file context>
Fix with Cubic

Copy link

@cubic-dev-ai cubic-dev-ai bot Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Client can become authenticated by supplying any token/user payload; backend never verifies the token before setting the session.

Prompt for AI agents
Address the following comment on app/Http/Controllers/Auth/LoginController.php at line 35:

<comment>Client can become authenticated by supplying any token/user payload; backend never verifies the token before setting the session.</comment>

<file context>
@@ -19,36 +19,63 @@ public function create(): View
+            $request-&gt;session()-&gt;regenerate();
+            $request-&gt;session()-&gt;put(&#39;api_token&#39;, $request-&gt;input(&#39;token&#39;));
+            $request-&gt;session()-&gt;put(&#39;user&#39;, $request-&gt;input(&#39;user&#39;));
+            $request-&gt;session()-&gt;put(&#39;authenticated&#39;, true);
+
+            if ($request-&gt;expectsJson()) {
</file context>
Fix with Cubic


if ($request->expectsJson()) {
return response()->json([
'success' => true,
'message' => 'Session created successfully',
'redirect' => route('dashboard', absolute: false)
]);
}

return redirect()->intended(route('dashboard', absolute: false));

} catch (\Exception $e) {
if ($request->expectsJson()) {
return response()->json([
'message' => 'Session creation failed',
'errors' => ['token' => ['Session creation failed']]
], 500);
}

throw ValidationException::withMessages([
'email' => trans('auth.failed'),
'token' => 'Session creation failed',
]);
}

RateLimiter::clear($this->throttleKey($request));

$request->session()->regenerate();

return redirect()->intended(route('dashboard', absolute: false));
}

public function destroy(Request $request): RedirectResponse
{
Auth::guard('web')->logout();
try {
// Call API logout if we have a token
$token = $request->session()->get('api_token');
if ($token) {
$apiUrl = config('api.url');
Http::timeout(config('api.timeout'))
->withToken($token)
->delete("{$apiUrl}/session");
}
} catch (\Exception $e) {
// Log error but continue with local logout
\Log::warning('API logout failed: ' . $e->getMessage());
}

// Clear session
$request->session()->invalidate();

$request->session()->regenerateToken();

return redirect('/');
Expand Down
54 changes: 0 additions & 54 deletions app/Http/Controllers/Auth/NewPasswordController.php

This file was deleted.

Loading