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
59 changes: 59 additions & 0 deletions doc/user_guide/PROXY_SERVER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Proxy Server for API Dash

API Dash (Web) runs directly in the browser, which means it is subject to Cross-Origin Resource Sharing (CORS) restrictions. Many APIs (like CoinMarketCap, Google, etc.) block requests from browser-based applications unless they are configured to allow them.

To bypass these restrictions, you can use a **CORS Proxy Server**. The proxy forwards your request to the target API and returns the response with the necessary CORS headers to satisfy the browser.

## How to Configure in API Dash

1. Go to **Settings**.
2. Look for the **Proxy URL** field (only visible on Web).
3. Enter the URL of your proxy server (e.g., `https://my-proxy.vercel.app/`).
4. API Dash will now prepend this URL to your API requests.

Example:
- Proxy URL: `https://my-proxy.com/`
- Target API: `https://api.example.com/v1/users`
- Actual Request: `https://my-proxy.com/https://api.example.com/v1/users`

## Hosting Your Own Proxy

API Dash does not provide a public proxy server. You should self-host one to ensure privacy and security.

### Node.js (Express + cors-anywhere)

You can easily deploy a proxy using [cors-anywhere](https://github.com/Rob--W/cors-anywhere).

1. Create a new directory and initialize a project:
```bash
mkdir my-proxy
cd my-proxy
npm init -y
npm install cors-anywhere
```

2. Create `server.js`:
```javascript
const host = process.env.HOST || '0.0.0.0';
const port = process.env.PORT || 8080;

const cors_proxy = require('cors-anywhere');
cors_proxy.createServer({
originWhitelist: [], // Allow all origins
requireHeader: ['origin', 'x-requested-with'],
removeHeaders: ['cookie', 'cookie2']
}).listen(port, host, function() {
console.log('Running CORS Anywhere on ' + host + ':' + port);
});
```

3. Run it:
```bash
node server.js
```

### Deploying to Vercel/Cloudflare

There are many open-source templates for deploying CORS proxies to serverless platforms like Vercel or Cloudflare Workers.

> **Security Note:** If you deploy a public proxy, anyone can use it to mask their IP. It is recommended to restrict the `originWhitelist` to your API Dash deployment domain (or `localhost` for testing).
97 changes: 10 additions & 87 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,100 +4,23 @@ import 'package:apidash_design_system/apidash_design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_portal/flutter_portal.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:window_manager/window_manager.dart' hide WindowCaption;
import 'widgets/widgets.dart' show WindowCaption, WorkspaceSelector;

import 'widgets/widgets.dart'
show WindowCaption, WorkspaceSelector, WindowListenerWrapper;
import 'providers/providers.dart';
import 'services/services.dart';
import 'screens/screens.dart';
import 'consts.dart';

class App extends ConsumerStatefulWidget {
class App extends ConsumerWidget {
const App({super.key});

@override
ConsumerState<App> createState() => _AppState();
}

class _AppState extends ConsumerState<App> with WindowListener {
@override
void initState() {
super.initState();
windowManager.addListener(this);
_init();
}

@override
void dispose() {
windowManager.removeListener(this);
super.dispose();
}

void _init() async {
// Add this line to override the default close handler
await windowManager.setPreventClose(true);
setState(() {});
}

@override
void onWindowResized() {
windowManager.getSize().then((value) {
ref.read(settingsProvider.notifier).update(size: value);
});
windowManager.getPosition().then((value) {
ref.read(settingsProvider.notifier).update(offset: value);
});
}

@override
void onWindowMoved() {
windowManager.getPosition().then((value) {
ref.read(settingsProvider.notifier).update(offset: value);
});
}

@override
void onWindowClose() async {
bool isPreventClose = await windowManager.isPreventClose();
if (isPreventClose) {
if (ref.watch(
settingsProvider.select((value) => value.promptBeforeClosing)) &&
ref.watch(hasUnsavedChangesProvider)) {
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Save Changes'),
content:
const Text('Want to save changes before you close API Dash?'),
actions: [
OutlinedButton(
child: const Text('No'),
onPressed: () async {
Navigator.of(context).pop();
await windowManager.destroy();
},
),
FilledButton(
child: const Text('Save'),
onPressed: () async {
await ref
.read(collectionStateNotifierProvider.notifier)
.saveData();
Navigator.of(context).pop();
await windowManager.destroy();
},
),
],
),
);
} else {
await windowManager.destroy();
}
}
}

@override
Widget build(BuildContext context) {
return context.isMediumWindow ? const MobileDashboard() : const Dashboard();
Widget build(BuildContext context, WidgetRef ref) {
return WindowListenerWrapper(
child:
context.isMediumWindow ? const MobileDashboard() : const Dashboard(),
);
}
}

Expand Down Expand Up @@ -128,7 +51,7 @@ class DashApp extends ConsumerWidget {
},
onCancel: () async {
try {
await windowManager.destroy();
await destroyWindow();
} catch (e) {
debugPrint(e.toString());
}
Expand Down
12 changes: 11 additions & 1 deletion lib/models/settings_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class SettingsModel {
this.isSSLDisabled = false,
this.isDashBotEnabled = true,
this.defaultAIModel,
this.proxyUrl,
});

final bool isDark;
Expand All @@ -36,6 +37,7 @@ class SettingsModel {
final bool isSSLDisabled;
final bool isDashBotEnabled;
final Map<String, Object?>? defaultAIModel;
final String? proxyUrl;

SettingsModel copyWith({
bool? isDark,
Expand All @@ -52,6 +54,7 @@ class SettingsModel {
bool? isSSLDisabled,
bool? isDashBotEnabled,
Map<String, Object?>? defaultAIModel,
String? proxyUrl,
}) {
return SettingsModel(
isDark: isDark ?? this.isDark,
Expand All @@ -70,6 +73,7 @@ class SettingsModel {
isSSLDisabled: isSSLDisabled ?? this.isSSLDisabled,
isDashBotEnabled: isDashBotEnabled ?? this.isDashBotEnabled,
defaultAIModel: defaultAIModel ?? this.defaultAIModel,
proxyUrl: proxyUrl ?? this.proxyUrl,
);
}

Expand All @@ -91,6 +95,7 @@ class SettingsModel {
isSSLDisabled: isSSLDisabled,
isDashBotEnabled: isDashBotEnabled,
defaultAIModel: defaultAIModel,
proxyUrl: proxyUrl,
);
}

Expand Down Expand Up @@ -149,6 +154,7 @@ class SettingsModel {
final defaultAIModel = data["defaultAIModel"] == null
? null
: Map<String, Object?>.from(data["defaultAIModel"]);
final proxyUrl = data["proxyUrl"] as String?;
const sm = SettingsModel();

return sm.copyWith(
Expand All @@ -167,6 +173,7 @@ class SettingsModel {
isSSLDisabled: isSSLDisabled,
isDashBotEnabled: isDashBotEnabled,
defaultAIModel: defaultAIModel,
proxyUrl: proxyUrl,
);
}

Expand All @@ -188,6 +195,7 @@ class SettingsModel {
"isSSLDisabled": isSSLDisabled,
"isDashBotEnabled": isDashBotEnabled,
"defaultAIModel": defaultAIModel,
"proxyUrl": proxyUrl,
};
}

Expand All @@ -214,7 +222,8 @@ class SettingsModel {
other.workspaceFolderPath == workspaceFolderPath &&
other.isSSLDisabled == isSSLDisabled &&
other.isDashBotEnabled == isDashBotEnabled &&
mapEquals(other.defaultAIModel, defaultAIModel);
mapEquals(other.defaultAIModel, defaultAIModel) &&
other.proxyUrl == proxyUrl;
}

@override
Expand All @@ -235,6 +244,7 @@ class SettingsModel {
isSSLDisabled,
isDashBotEnabled,
defaultAIModel,
proxyUrl,
);
}
}
17 changes: 15 additions & 2 deletions lib/providers/collection_providers.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:apidash_core/apidash_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand Down Expand Up @@ -387,12 +388,16 @@ class CollectionStateNotifier
};
bool streamingMode = true; //Default: Streaming First

final String? proxyUrl =
kIsWeb ? ref.read(settingsProvider).proxyUrl : null;

final stream = await streamHttpRequest(
requestId,
apiType,
substitutedHttpRequestModel,
defaultUriScheme: defaultUriScheme,
noSSL: noSSL,
proxyUrl: proxyUrl,
);

HttpResponseModel? httpResponseModel;
Expand Down Expand Up @@ -472,13 +477,21 @@ class CollectionStateNotifier
final (response, duration, errorMessage) = await completer.future;

if (response == null) {
String? msg = errorMessage;
if (kIsWeb &&
(errorMessage?.contains("XMLHttpRequest") == true ||
errorMessage?.contains("ClientException") == true)) {
msg =
"$errorMessage\n\nError can be due to CORS restrictions. Check if the API supports CORS or use a Proxy (Settings > Proxy URL)";
}

newRequestModel = newRequestModel.copyWith(
responseStatus: -1,
message: errorMessage,
message: msg,
isWorking: false,
isStreaming: false,
);
terminal.failNetwork(logId, errorMessage ?? 'Unknown error');
terminal.failNetwork(logId, msg ?? 'Unknown error');
} else {
final statusCode = response.statusCode;
httpResponseModel = baseHttpResponseModel.fromResponse(
Expand Down
8 changes: 4 additions & 4 deletions lib/providers/js_runtime_notifier.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'dart:convert';
import 'package:apidash_core/apidash_core.dart';
import 'package:flutter/services.dart';
import 'package:flutter_js/flutter_js.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../services/js_runtime/app_js_runtime.dart';
import '../models/models.dart';
import '../utils/utils.dart';
import '../providers/terminal_providers.dart';
Expand Down Expand Up @@ -41,12 +41,12 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {
JsRuntimeNotifier(this.ref) : super(const JsRuntimeState());

final Ref ref;
late final JavascriptRuntime _runtime;
late final AppJsRuntime _runtime;
String? _currentRequestId;

void _initialize() {
if (state.initialized) return;
_runtime = getJavascriptRuntime();
_runtime = getAppJavascriptRuntime();
_setupJsBridge();
state = state.copyWith(initialized: true);
}
Expand All @@ -64,7 +64,7 @@ class JsRuntimeNotifier extends StateNotifier<JsRuntimeState> {
super.dispose();
}

JsEvalResult evaluate(String code) {
AppJsEvalResult evaluate(String code) {
// If disposed, prevent usage
if (!mounted) {
throw StateError('JsRuntimeNotifier used after dispose');
Expand Down
2 changes: 2 additions & 0 deletions lib/providers/settings_providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class ThemeStateNotifier extends StateNotifier<SettingsModel> {
bool? isSSLDisabled,
bool? isDashBotEnabled,
Map<String, Object?>? defaultAIModel,
String? proxyUrl,
}) async {
state = state.copyWith(
isDark: isDark,
Expand All @@ -51,6 +52,7 @@ class ThemeStateNotifier extends StateNotifier<SettingsModel> {
isSSLDisabled: isSSLDisabled,
isDashBotEnabled: isDashBotEnabled,
defaultAIModel: defaultAIModel,
proxyUrl: proxyUrl,
);
await setSettingsToSharedPrefs(state);
}
Expand Down
29 changes: 29 additions & 0 deletions lib/screens/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,35 @@ class SettingsPage extends ConsumerWidget {
},
)
: kSizedBoxEmpty,
kIsWeb
? ListTile(
hoverColor: kColorTransparent,
title: const Text('Proxy URL'),
subtitle: const Text(
'Enter the URL of the CORS proxy server to bypass CORS restrictions on the web.'),
trailing: SizedBox(
width: 300,
child: TextField(
onChanged: (value) {
ref
.read(settingsProvider.notifier)
.update(proxyUrl: value);
},
controller:
TextEditingController(text: settings.proxyUrl),
decoration: const InputDecoration(
hintText: 'https://proxy.com/',
isDense: false,
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
),
),
),
)
: kSizedBoxEmpty,
ListTile(
hoverColor: kColorTransparent,
title: const Text('Default Code Generator'),
Expand Down
Loading