@theihtisham/devtools-with-cloud 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +15 -0
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/docker-compose.yml +23 -0
- package/jest.config.js +7 -0
- package/next-env.d.ts +5 -0
- package/next.config.mjs +22 -0
- package/package.json +82 -0
- package/postcss.config.js +6 -0
- package/prisma/schema.prisma +105 -0
- package/prisma/seed.ts +211 -0
- package/src/app/(app)/ai/page.tsx +122 -0
- package/src/app/(app)/collections/page.tsx +155 -0
- package/src/app/(app)/environments/page.tsx +96 -0
- package/src/app/(app)/history/page.tsx +107 -0
- package/src/app/(app)/import/page.tsx +102 -0
- package/src/app/(app)/layout.tsx +60 -0
- package/src/app/(app)/settings/page.tsx +79 -0
- package/src/app/(app)/workspace/page.tsx +284 -0
- package/src/app/api/ai/discover/route.ts +17 -0
- package/src/app/api/ai/explain/route.ts +29 -0
- package/src/app/api/ai/generate-tests/route.ts +37 -0
- package/src/app/api/ai/suggest/route.ts +29 -0
- package/src/app/api/collections/[id]/route.ts +66 -0
- package/src/app/api/collections/route.ts +48 -0
- package/src/app/api/environments/route.ts +40 -0
- package/src/app/api/export/openapi/route.ts +17 -0
- package/src/app/api/export/postman/route.ts +18 -0
- package/src/app/api/import/curl/route.ts +18 -0
- package/src/app/api/import/har/route.ts +20 -0
- package/src/app/api/import/openapi/route.ts +21 -0
- package/src/app/api/import/postman/route.ts +21 -0
- package/src/app/api/proxy/route.ts +35 -0
- package/src/app/api/requests/[id]/execute/route.ts +85 -0
- package/src/app/api/requests/[id]/history/route.ts +23 -0
- package/src/app/api/requests/[id]/route.ts +66 -0
- package/src/app/api/requests/route.ts +49 -0
- package/src/app/api/workspaces/route.ts +38 -0
- package/src/app/globals.css +99 -0
- package/src/app/layout.tsx +24 -0
- package/src/app/page.tsx +182 -0
- package/src/components/ai/ai-panel.tsx +65 -0
- package/src/components/ai/code-explainer.tsx +51 -0
- package/src/components/ai/endpoint-discovery.tsx +62 -0
- package/src/components/ai/test-generator.tsx +49 -0
- package/src/components/collections/collection-actions.tsx +36 -0
- package/src/components/collections/collection-tree.tsx +55 -0
- package/src/components/collections/folder-creator.tsx +54 -0
- package/src/components/landing/comparison.tsx +43 -0
- package/src/components/landing/cta.tsx +16 -0
- package/src/components/landing/features.tsx +24 -0
- package/src/components/landing/hero.tsx +23 -0
- package/src/components/response/body-viewer.tsx +33 -0
- package/src/components/response/headers-viewer.tsx +23 -0
- package/src/components/response/status-badge.tsx +25 -0
- package/src/components/response/test-results.tsx +50 -0
- package/src/components/response/timing-chart.tsx +39 -0
- package/src/components/ui/badge.tsx +24 -0
- package/src/components/ui/button.tsx +32 -0
- package/src/components/ui/code-editor.tsx +51 -0
- package/src/components/ui/dialog.tsx +56 -0
- package/src/components/ui/dropdown.tsx +63 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/key-value-editor.tsx +75 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/tabs.tsx +85 -0
- package/src/components/ui/textarea.tsx +22 -0
- package/src/components/ui/toast.tsx +54 -0
- package/src/components/workspace/request-panel.tsx +38 -0
- package/src/components/workspace/response-panel.tsx +81 -0
- package/src/components/workspace/sidebar.tsx +52 -0
- package/src/components/workspace/split-pane.tsx +49 -0
- package/src/components/workspace/tabs/auth-tab.tsx +94 -0
- package/src/components/workspace/tabs/body-tab.tsx +41 -0
- package/src/components/workspace/tabs/headers-tab.tsx +23 -0
- package/src/components/workspace/tabs/params-tab.tsx +23 -0
- package/src/components/workspace/tabs/pre-request-tab.tsx +26 -0
- package/src/components/workspace/url-bar.tsx +53 -0
- package/src/hooks/use-ai.ts +115 -0
- package/src/hooks/use-collection.ts +71 -0
- package/src/hooks/use-environment.ts +73 -0
- package/src/hooks/use-request.ts +111 -0
- package/src/lib/ai/endpoint-discovery.ts +158 -0
- package/src/lib/ai/explainer.ts +127 -0
- package/src/lib/ai/suggester.ts +164 -0
- package/src/lib/ai/test-generator.ts +161 -0
- package/src/lib/auth/api-key.ts +28 -0
- package/src/lib/auth/aws-sig.ts +131 -0
- package/src/lib/auth/basic.ts +17 -0
- package/src/lib/auth/bearer.ts +15 -0
- package/src/lib/auth/oauth2.ts +155 -0
- package/src/lib/auth/types.ts +16 -0
- package/src/lib/db/client.ts +15 -0
- package/src/lib/env/manager.ts +32 -0
- package/src/lib/env/resolver.ts +30 -0
- package/src/lib/exporters/openapi.ts +193 -0
- package/src/lib/exporters/postman.ts +140 -0
- package/src/lib/graphql/builder.ts +249 -0
- package/src/lib/graphql/formatter.ts +147 -0
- package/src/lib/graphql/index.ts +43 -0
- package/src/lib/graphql/introspection.ts +175 -0
- package/src/lib/graphql/types.ts +99 -0
- package/src/lib/graphql/validator.ts +216 -0
- package/src/lib/http/client.ts +112 -0
- package/src/lib/http/proxy.ts +83 -0
- package/src/lib/http/request-builder.ts +214 -0
- package/src/lib/http/response-parser.ts +106 -0
- package/src/lib/http/timing.ts +63 -0
- package/src/lib/importers/curl-parser.ts +346 -0
- package/src/lib/importers/har-parser.ts +128 -0
- package/src/lib/importers/openapi.ts +324 -0
- package/src/lib/importers/postman.ts +312 -0
- package/src/lib/test-runner/assertions.ts +163 -0
- package/src/lib/test-runner/reporter.ts +90 -0
- package/src/lib/test-runner/runner.ts +69 -0
- package/src/lib/utils/api-response.ts +85 -0
- package/src/lib/utils/cn.ts +6 -0
- package/src/lib/utils/content-type.ts +123 -0
- package/src/lib/utils/download.ts +53 -0
- package/src/lib/utils/errors.ts +92 -0
- package/src/lib/utils/format.ts +142 -0
- package/src/lib/utils/syntax-highlight.ts +108 -0
- package/src/lib/utils/validation.ts +231 -0
- package/src/lib/websocket/client.ts +182 -0
- package/src/lib/websocket/frames.ts +96 -0
- package/src/lib/websocket/history.ts +121 -0
- package/src/lib/websocket/index.ts +25 -0
- package/src/lib/websocket/types.ts +57 -0
- package/src/types/ai.ts +28 -0
- package/src/types/collection.ts +24 -0
- package/src/types/environment.ts +16 -0
- package/src/types/request.ts +54 -0
- package/src/types/response.ts +37 -0
- package/tailwind.config.ts +82 -0
- package/tests/lib/env/resolver.test.ts +108 -0
- package/tests/lib/graphql/builder.test.ts +349 -0
- package/tests/lib/graphql/formatter.test.ts +99 -0
- package/tests/lib/http/request-builder.test.ts +160 -0
- package/tests/lib/http/response-parser.test.ts +150 -0
- package/tests/lib/http/timing.test.ts +188 -0
- package/tests/lib/importers/curl-parser.test.ts +245 -0
- package/tests/lib/test-runner/assertions.test.ts +342 -0
- package/tests/lib/utils/cn.test.ts +46 -0
- package/tests/lib/utils/content-type.test.ts +175 -0
- package/tests/lib/utils/format.test.ts +188 -0
- package/tests/lib/utils/validation.test.ts +237 -0
- package/tests/lib/websocket/history.test.ts +186 -0
- package/tsconfig.json +29 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +21 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
export default function SettingsPage() {
|
|
6
|
+
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
|
|
7
|
+
const [defaultTimeout, setDefaultTimeout] = useState('30000');
|
|
8
|
+
const [followRedirects, setFollowRedirects] = useState(true);
|
|
9
|
+
const [verifySSL, setVerifySSL] = useState(true);
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="p-6 max-w-2xl">
|
|
13
|
+
<h1 className="text-2xl font-bold text-foreground mb-6">Settings</h1>
|
|
14
|
+
|
|
15
|
+
<div className="space-y-6">
|
|
16
|
+
{/* Appearance */}
|
|
17
|
+
<section>
|
|
18
|
+
<h2 className="text-lg font-semibold text-foreground mb-3">Appearance</h2>
|
|
19
|
+
<div className="space-y-3">
|
|
20
|
+
<div className="flex items-center justify-between">
|
|
21
|
+
<label className="text-sm text-foreground">Theme</label>
|
|
22
|
+
<select
|
|
23
|
+
value={theme}
|
|
24
|
+
onChange={(e) => setTheme(e.target.value as 'dark' | 'light')}
|
|
25
|
+
className="px-3 py-1.5 rounded-md border border-border bg-background text-foreground text-sm"
|
|
26
|
+
>
|
|
27
|
+
<option value="dark">Dark</option>
|
|
28
|
+
<option value="light">Light</option>
|
|
29
|
+
</select>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</section>
|
|
33
|
+
|
|
34
|
+
{/* Request Defaults */}
|
|
35
|
+
<section>
|
|
36
|
+
<h2 className="text-lg font-semibold text-foreground mb-3">Request Defaults</h2>
|
|
37
|
+
<div className="space-y-3">
|
|
38
|
+
<div className="flex items-center justify-between">
|
|
39
|
+
<label className="text-sm text-foreground">Default Timeout (ms)</label>
|
|
40
|
+
<input
|
|
41
|
+
type="number"
|
|
42
|
+
value={defaultTimeout}
|
|
43
|
+
onChange={(e) => setDefaultTimeout(e.target.value)}
|
|
44
|
+
className="w-32 px-3 py-1.5 rounded-md border border-border bg-background text-foreground text-sm text-right"
|
|
45
|
+
/>
|
|
46
|
+
</div>
|
|
47
|
+
<div className="flex items-center justify-between">
|
|
48
|
+
<label className="text-sm text-foreground">Follow Redirects</label>
|
|
49
|
+
<button
|
|
50
|
+
onClick={() => setFollowRedirects(!followRedirects)}
|
|
51
|
+
className={`w-10 h-6 rounded-full transition-colors ${followRedirects ? 'bg-primary' : 'bg-border'}`}
|
|
52
|
+
>
|
|
53
|
+
<span className={`block w-4 h-4 rounded-full bg-white transition-transform ${followRedirects ? 'translate-x-5' : 'translate-x-1'}`} />
|
|
54
|
+
</button>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="flex items-center justify-between">
|
|
57
|
+
<label className="text-sm text-foreground">Verify SSL</label>
|
|
58
|
+
<button
|
|
59
|
+
onClick={() => setVerifySSL(!verifySSL)}
|
|
60
|
+
className={`w-10 h-6 rounded-full transition-colors ${verifySSL ? 'bg-primary' : 'bg-border'}`}
|
|
61
|
+
>
|
|
62
|
+
<span className={`block w-4 h-4 rounded-full bg-white transition-transform ${verifySSL ? 'translate-x-5' : 'translate-x-1'}`} />
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
</section>
|
|
67
|
+
|
|
68
|
+
{/* About */}
|
|
69
|
+
<section>
|
|
70
|
+
<h2 className="text-lg font-semibold text-foreground mb-3">About</h2>
|
|
71
|
+
<div className="text-sm text-muted-foreground space-y-1">
|
|
72
|
+
<p>APITester v1.0.0</p>
|
|
73
|
+
<p>Open-source API testing tool with AI-powered features</p>
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
import type { HttpMethod, KeyValue } from '@/lib/utils/validation';
|
|
5
|
+
import { cn } from '@/lib/utils/cn';
|
|
6
|
+
|
|
7
|
+
const METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
8
|
+
|
|
9
|
+
interface RequestState {
|
|
10
|
+
method: HttpMethod;
|
|
11
|
+
url: string;
|
|
12
|
+
headers: KeyValue[];
|
|
13
|
+
params: KeyValue[];
|
|
14
|
+
body: string;
|
|
15
|
+
bodyType: string;
|
|
16
|
+
activeTab: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ResponseState = {
|
|
20
|
+
status: number;
|
|
21
|
+
statusText: string;
|
|
22
|
+
headers: Record<string, string>;
|
|
23
|
+
body: string;
|
|
24
|
+
duration: number;
|
|
25
|
+
size: number;
|
|
26
|
+
} | null;
|
|
27
|
+
|
|
28
|
+
export default function WorkspacePage() {
|
|
29
|
+
const [request, setRequest] = useState<RequestState>({
|
|
30
|
+
method: 'GET',
|
|
31
|
+
url: '',
|
|
32
|
+
headers: [],
|
|
33
|
+
params: [],
|
|
34
|
+
body: '',
|
|
35
|
+
bodyType: 'none',
|
|
36
|
+
activeTab: 'params',
|
|
37
|
+
});
|
|
38
|
+
const [response, setResponse] = useState<ResponseState>(null);
|
|
39
|
+
const [loading, setLoading] = useState(false);
|
|
40
|
+
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
|
|
42
|
+
const sendRequest = useCallback(async () => {
|
|
43
|
+
if (!request.url.trim()) return;
|
|
44
|
+
setLoading(true);
|
|
45
|
+
setError(null);
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const start = performance.now();
|
|
49
|
+
const headerMap: Record<string, string> = {};
|
|
50
|
+
for (const h of request.headers) {
|
|
51
|
+
if (h.enabled && h.key) {
|
|
52
|
+
headerMap[h.key] = h.value;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let body: string | undefined;
|
|
57
|
+
if (request.bodyType !== 'none' && request.method !== 'GET' && request.method !== 'HEAD') {
|
|
58
|
+
body = request.body;
|
|
59
|
+
if (request.bodyType === 'json' && !headerMap['Content-Type']) {
|
|
60
|
+
headerMap['Content-Type'] = 'application/json';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const res = await fetch('/api/proxy', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'Content-Type': 'application/json' },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
url: request.url,
|
|
69
|
+
method: request.method,
|
|
70
|
+
headers: headerMap,
|
|
71
|
+
body,
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const data = await res.json();
|
|
76
|
+
const elapsed = Math.round(performance.now() - start);
|
|
77
|
+
|
|
78
|
+
const responseHeaders: Record<string, string> = {};
|
|
79
|
+
if (data.headers) {
|
|
80
|
+
for (const [key, value] of Object.entries(data.headers as Record<string, string>)) {
|
|
81
|
+
responseHeaders[key] = value;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
setResponse({
|
|
86
|
+
status: data.status ?? res.status,
|
|
87
|
+
statusText: data.statusText ?? res.statusText,
|
|
88
|
+
headers: responseHeaders,
|
|
89
|
+
body: typeof data.body === 'string' ? data.body : JSON.stringify(data.body, null, 2),
|
|
90
|
+
duration: elapsed,
|
|
91
|
+
size: typeof data.body === 'string' ? new TextEncoder().encode(data.body).length : 0,
|
|
92
|
+
});
|
|
93
|
+
} catch (err) {
|
|
94
|
+
setError(err instanceof Error ? err.message : 'Request failed');
|
|
95
|
+
} finally {
|
|
96
|
+
setLoading(false);
|
|
97
|
+
}
|
|
98
|
+
}, [request]);
|
|
99
|
+
|
|
100
|
+
const updateMethod = (method: HttpMethod) => {
|
|
101
|
+
setRequest((prev) => ({ ...prev, method }));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const updateUrl = (url: string) => {
|
|
105
|
+
setRequest((prev) => ({ ...prev, url }));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className="flex flex-col h-full">
|
|
110
|
+
{/* URL Bar */}
|
|
111
|
+
<div className="flex items-center gap-2 p-3 border-b border-border">
|
|
112
|
+
<select
|
|
113
|
+
value={request.method}
|
|
114
|
+
onChange={(e) => updateMethod(e.target.value as HttpMethod)}
|
|
115
|
+
className={cn(
|
|
116
|
+
'px-3 py-2 rounded-md border border-border bg-background text-sm font-medium font-mono',
|
|
117
|
+
request.method === 'GET' && 'text-method-get',
|
|
118
|
+
request.method === 'POST' && 'text-method-post',
|
|
119
|
+
request.method === 'PUT' && 'text-method-put',
|
|
120
|
+
request.method === 'DELETE' && 'text-method-delete',
|
|
121
|
+
request.method === 'PATCH' && 'text-method-patch',
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{METHODS.map((m) => (
|
|
125
|
+
<option key={m} value={m}>{m}</option>
|
|
126
|
+
))}
|
|
127
|
+
</select>
|
|
128
|
+
<input
|
|
129
|
+
type="text"
|
|
130
|
+
value={request.url}
|
|
131
|
+
onChange={(e) => updateUrl(e.target.value)}
|
|
132
|
+
onKeyDown={(e) => e.key === 'Enter' && sendRequest()}
|
|
133
|
+
placeholder="Enter request URL (e.g., https://api.example.com/users)"
|
|
134
|
+
className="flex-1 px-3 py-2 rounded-md border border-border bg-background text-foreground text-sm font-mono placeholder:text-muted-foreground"
|
|
135
|
+
/>
|
|
136
|
+
<button
|
|
137
|
+
onClick={sendRequest}
|
|
138
|
+
disabled={loading || !request.url.trim()}
|
|
139
|
+
className="px-6 py-2 rounded-md bg-primary text-primary-foreground font-medium text-sm hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
140
|
+
>
|
|
141
|
+
{loading ? 'Sending...' : 'Send'}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
{/* Request/Response Split */}
|
|
146
|
+
<div className="flex flex-1 overflow-hidden">
|
|
147
|
+
{/* Request Panel */}
|
|
148
|
+
<div className="w-1/2 border-r border-border flex flex-col overflow-hidden">
|
|
149
|
+
{/* Tabs */}
|
|
150
|
+
<div className="flex border-b border-border">
|
|
151
|
+
{['params', 'headers', 'body', 'auth'].map((tab) => (
|
|
152
|
+
<button
|
|
153
|
+
key={tab}
|
|
154
|
+
onClick={() => setRequest((prev) => ({ ...prev, activeTab: tab }))}
|
|
155
|
+
className={cn(
|
|
156
|
+
'px-4 py-2 text-sm capitalize transition-colors',
|
|
157
|
+
request.activeTab === tab
|
|
158
|
+
? 'text-primary border-b-2 border-primary'
|
|
159
|
+
: 'text-muted-foreground hover:text-foreground',
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
{tab}
|
|
163
|
+
</button>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
{/* Tab Content */}
|
|
168
|
+
<div className="flex-1 overflow-auto p-4">
|
|
169
|
+
{request.activeTab === 'params' && (
|
|
170
|
+
<div>
|
|
171
|
+
<div className="text-sm text-muted-foreground mb-2">Query Parameters</div>
|
|
172
|
+
<textarea
|
|
173
|
+
value={request.headers.map((h) => `${h.key}: ${h.value}`).join('\n')}
|
|
174
|
+
readOnly
|
|
175
|
+
className="w-full h-32 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm"
|
|
176
|
+
placeholder="No parameters"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
{request.activeTab === 'headers' && (
|
|
181
|
+
<div>
|
|
182
|
+
<div className="text-sm text-muted-foreground mb-2">Request Headers</div>
|
|
183
|
+
<textarea
|
|
184
|
+
value={request.headers.map((h) => `${h.key}: ${h.value}`).join('\n')}
|
|
185
|
+
readOnly
|
|
186
|
+
className="w-full h-32 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm"
|
|
187
|
+
placeholder="No headers"
|
|
188
|
+
/>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
{request.activeTab === 'body' && (
|
|
192
|
+
<div>
|
|
193
|
+
<div className="flex items-center gap-2 mb-2">
|
|
194
|
+
<select
|
|
195
|
+
value={request.bodyType}
|
|
196
|
+
onChange={(e) => setRequest((prev) => ({ ...prev, bodyType: e.target.value }))}
|
|
197
|
+
className="px-2 py-1 rounded border border-border bg-background text-sm text-foreground"
|
|
198
|
+
>
|
|
199
|
+
<option value="none">none</option>
|
|
200
|
+
<option value="json">JSON</option>
|
|
201
|
+
<option value="text">Text</option>
|
|
202
|
+
<option value="xml">XML</option>
|
|
203
|
+
<option value="urlencoded">Form URL Encoded</option>
|
|
204
|
+
</select>
|
|
205
|
+
</div>
|
|
206
|
+
{request.bodyType !== 'none' && (
|
|
207
|
+
<textarea
|
|
208
|
+
value={request.body}
|
|
209
|
+
onChange={(e) => setRequest((prev) => ({ ...prev, body: e.target.value }))}
|
|
210
|
+
className="w-full h-64 px-3 py-2 rounded-md border border-border bg-background text-foreground font-mono text-sm resize-y"
|
|
211
|
+
placeholder={`Enter ${request.bodyType} body...`}
|
|
212
|
+
/>
|
|
213
|
+
)}
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
{request.activeTab === 'auth' && (
|
|
217
|
+
<div className="text-sm text-muted-foreground">
|
|
218
|
+
<p>Configure authentication in the request settings.</p>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Response Panel */}
|
|
225
|
+
<div className="w-1/2 flex flex-col overflow-hidden">
|
|
226
|
+
{error && (
|
|
227
|
+
<div className="p-3 bg-destructive/10 text-destructive text-sm border-b border-border">
|
|
228
|
+
{error}
|
|
229
|
+
</div>
|
|
230
|
+
)}
|
|
231
|
+
{response ? (
|
|
232
|
+
<>
|
|
233
|
+
<div className="flex items-center gap-4 p-3 border-b border-border">
|
|
234
|
+
<span className={cn(
|
|
235
|
+
'px-2 py-1 rounded text-sm font-mono font-medium',
|
|
236
|
+
response.status < 300 && 'bg-green-500/10 text-green-400',
|
|
237
|
+
response.status >= 300 && response.status < 400 && 'bg-blue-500/10 text-blue-400',
|
|
238
|
+
response.status >= 400 && response.status < 500 && 'bg-orange-500/10 text-orange-400',
|
|
239
|
+
response.status >= 500 && 'bg-red-500/10 text-red-400',
|
|
240
|
+
)}>
|
|
241
|
+
{response.status} {response.statusText}
|
|
242
|
+
</span>
|
|
243
|
+
<span className="text-sm text-muted-foreground">
|
|
244
|
+
{response.duration}ms
|
|
245
|
+
</span>
|
|
246
|
+
<span className="text-sm text-muted-foreground">
|
|
247
|
+
{formatBytes(response.size)}
|
|
248
|
+
</span>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="flex-1 overflow-auto p-4">
|
|
251
|
+
<pre className="text-sm font-mono text-foreground whitespace-pre-wrap break-all">
|
|
252
|
+
{tryFormatJson(response.body)}
|
|
253
|
+
</pre>
|
|
254
|
+
</div>
|
|
255
|
+
</>
|
|
256
|
+
) : (
|
|
257
|
+
<div className="flex-1 flex items-center justify-center text-muted-foreground">
|
|
258
|
+
<div className="text-center">
|
|
259
|
+
<p className="text-lg mb-2">No response yet</p>
|
|
260
|
+
<p className="text-sm">Enter a URL and click Send to make a request</p>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function formatBytes(bytes: number): string {
|
|
271
|
+
if (bytes === 0) return '0 B';
|
|
272
|
+
const k = 1024;
|
|
273
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
274
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
275
|
+
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${units[i] ?? 'B'}`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function tryFormatJson(body: string): string {
|
|
279
|
+
try {
|
|
280
|
+
return JSON.stringify(JSON.parse(body), null, 2);
|
|
281
|
+
} catch {
|
|
282
|
+
return body;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { success, error } from '@/lib/utils/api-response';
|
|
3
|
+
import { AiDiscoverSchema } from '@/lib/utils/validation';
|
|
4
|
+
import { discoverEndpoints } from '@/lib/ai/endpoint-discovery';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
7
|
+
try {
|
|
8
|
+
const body = await req.json();
|
|
9
|
+
const parsed = AiDiscoverSchema.parse(body);
|
|
10
|
+
|
|
11
|
+
const endpoints = discoverEndpoints(parsed.code, parsed.framework);
|
|
12
|
+
|
|
13
|
+
return success({ endpoints, count: endpoints.length }) as Response;
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return error(err) as Response;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { success, error } from '@/lib/utils/api-response';
|
|
3
|
+
import { AiExplainSchema } from '@/lib/utils/validation';
|
|
4
|
+
import { explainResponse } from '@/lib/ai/explainer';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
7
|
+
try {
|
|
8
|
+
const body = await req.json();
|
|
9
|
+
const parsed = AiExplainSchema.parse(body);
|
|
10
|
+
|
|
11
|
+
const responseData = typeof parsed.response === 'object' && parsed.response !== null && 'status' in parsed.response
|
|
12
|
+
? parsed.response as { status: number; statusText: string; headers: Record<string, string>; body: string }
|
|
13
|
+
: { status: 200, statusText: 'OK', headers: {}, body: JSON.stringify(parsed.response) };
|
|
14
|
+
|
|
15
|
+
const explanation = await explainResponse(
|
|
16
|
+
{
|
|
17
|
+
response: responseData,
|
|
18
|
+
question: parsed.question,
|
|
19
|
+
},
|
|
20
|
+
process.env['OPENAI_API_KEY'],
|
|
21
|
+
process.env['OPENAI_BASE_URL'],
|
|
22
|
+
process.env['OPENAI_MODEL'],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return success({ explanation }) as Response;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return error(err) as Response;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { success, error } from '@/lib/utils/api-response';
|
|
3
|
+
import { AiGenerateTestsSchema } from '@/lib/utils/validation';
|
|
4
|
+
import { generateTests } from '@/lib/ai/test-generator';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
7
|
+
try {
|
|
8
|
+
const body = await req.json();
|
|
9
|
+
const parsed = AiGenerateTestsSchema.parse(body);
|
|
10
|
+
|
|
11
|
+
const tests = await generateTests(
|
|
12
|
+
{
|
|
13
|
+
method: 'GET',
|
|
14
|
+
url: '',
|
|
15
|
+
headers: {},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
status: typeof parsed.response === 'object' && parsed.response !== null && 'status' in parsed.response
|
|
19
|
+
? (parsed.response as { status: number }).status
|
|
20
|
+
: 200,
|
|
21
|
+
statusText: 'OK',
|
|
22
|
+
headers: {},
|
|
23
|
+
body: typeof parsed.response === 'string'
|
|
24
|
+
? parsed.response
|
|
25
|
+
: JSON.stringify(parsed.response, null, 2),
|
|
26
|
+
duration: 0,
|
|
27
|
+
},
|
|
28
|
+
process.env['OPENAI_API_KEY'],
|
|
29
|
+
process.env['OPENAI_BASE_URL'],
|
|
30
|
+
process.env['OPENAI_MODEL'],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return success({ tests }) as Response;
|
|
34
|
+
} catch (err) {
|
|
35
|
+
return error(err) as Response;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { success, error } from '@/lib/utils/api-response';
|
|
3
|
+
import { AiSuggestSchema } from '@/lib/utils/validation';
|
|
4
|
+
import { suggestImprovements } from '@/lib/ai/suggester';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
7
|
+
try {
|
|
8
|
+
const body = await req.json();
|
|
9
|
+
const parsed = AiSuggestSchema.parse(body);
|
|
10
|
+
|
|
11
|
+
const requestData = typeof parsed.request === 'object' && parsed.request !== null
|
|
12
|
+
? parsed.request as { method: string; url: string; headers: Record<string, string>; body?: string; bodyType?: string; auth?: unknown }
|
|
13
|
+
: { method: 'GET', url: String(parsed.request), headers: {} };
|
|
14
|
+
|
|
15
|
+
const suggestions = await suggestImprovements(
|
|
16
|
+
{
|
|
17
|
+
request: requestData,
|
|
18
|
+
response: parsed.response as { status: number; statusText: string; headers: Record<string, string>; body: string; duration: number } | undefined,
|
|
19
|
+
},
|
|
20
|
+
process.env['OPENAI_API_KEY'],
|
|
21
|
+
process.env['OPENAI_BASE_URL'],
|
|
22
|
+
process.env['OPENAI_MODEL'],
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
return success({ suggestions }) as Response;
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return error(err) as Response;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/db/client';
|
|
3
|
+
import { success, noContent, error } from '@/lib/utils/api-response';
|
|
4
|
+
import { NotFoundError } from '@/lib/utils/errors';
|
|
5
|
+
import { UpdateCollectionSchema } from '@/lib/utils/validation';
|
|
6
|
+
|
|
7
|
+
interface RouteContext {
|
|
8
|
+
params: { id: string };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function GET(_req: NextRequest, context: RouteContext): Promise<Response> {
|
|
12
|
+
try {
|
|
13
|
+
const { id } = context.params;
|
|
14
|
+
const collection = await prisma.collection.findUnique({
|
|
15
|
+
where: { id },
|
|
16
|
+
include: {
|
|
17
|
+
requests: { orderBy: { sortOrder: 'asc' } },
|
|
18
|
+
children: { include: { requests: { orderBy: { sortOrder: 'asc' } } } },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!collection) {
|
|
23
|
+
throw new NotFoundError('Collection', id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return success(collection) as Response;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
return error(err) as Response;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function PUT(req: NextRequest, context: RouteContext): Promise<Response> {
|
|
33
|
+
try {
|
|
34
|
+
const { id } = context.params;
|
|
35
|
+
const body = await req.json();
|
|
36
|
+
const parsed = UpdateCollectionSchema.parse({ ...body, id });
|
|
37
|
+
|
|
38
|
+
const collection = await prisma.collection.update({
|
|
39
|
+
where: { id },
|
|
40
|
+
data: {
|
|
41
|
+
name: parsed.name,
|
|
42
|
+
description: parsed.description,
|
|
43
|
+
parentId: parsed.parentId,
|
|
44
|
+
variables: parsed.variables,
|
|
45
|
+
preScript: parsed.preScript,
|
|
46
|
+
postScript: parsed.postScript,
|
|
47
|
+
sortOrder: parsed.sortOrder,
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return success(collection) as Response;
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return error(err) as Response;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function DELETE(_req: NextRequest, context: RouteContext): Promise<Response> {
|
|
58
|
+
try {
|
|
59
|
+
const { id } = context.params;
|
|
60
|
+
|
|
61
|
+
await prisma.collection.delete({ where: { id } });
|
|
62
|
+
return noContent() as Response;
|
|
63
|
+
} catch (err) {
|
|
64
|
+
return error(err) as Response;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/db/client';
|
|
3
|
+
import { success, created, error } from '@/lib/utils/api-response';
|
|
4
|
+
import { CreateCollectionSchema } from '@/lib/utils/validation';
|
|
5
|
+
|
|
6
|
+
export async function GET(req: NextRequest): Promise<Response> {
|
|
7
|
+
try {
|
|
8
|
+
const { searchParams } = new URL(req.url);
|
|
9
|
+
const workspaceId = searchParams.get('workspaceId');
|
|
10
|
+
|
|
11
|
+
const collections = await prisma.collection.findMany({
|
|
12
|
+
where: workspaceId ? { workspaceId, parentId: null } : { parentId: null },
|
|
13
|
+
include: {
|
|
14
|
+
requests: { orderBy: { sortOrder: 'asc' } },
|
|
15
|
+
children: { include: { requests: { orderBy: { sortOrder: 'asc' } } } },
|
|
16
|
+
},
|
|
17
|
+
orderBy: { sortOrder: 'asc' },
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
return success(collections) as Response;
|
|
21
|
+
} catch (err) {
|
|
22
|
+
return error(err) as Response;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
27
|
+
try {
|
|
28
|
+
const body = await req.json();
|
|
29
|
+
const parsed = CreateCollectionSchema.parse(body);
|
|
30
|
+
|
|
31
|
+
const collection = await prisma.collection.create({
|
|
32
|
+
data: {
|
|
33
|
+
name: parsed.name,
|
|
34
|
+
description: parsed.description,
|
|
35
|
+
workspaceId: parsed.workspaceId,
|
|
36
|
+
parentId: parsed.parentId,
|
|
37
|
+
variables: parsed.variables,
|
|
38
|
+
preScript: parsed.preScript,
|
|
39
|
+
postScript: parsed.postScript,
|
|
40
|
+
sortOrder: parsed.sortOrder,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return created(collection) as Response;
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return error(err) as Response;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { prisma } from '@/lib/db/client';
|
|
3
|
+
import { success, created, error } from '@/lib/utils/api-response';
|
|
4
|
+
import { CreateEnvironmentSchema } from '@/lib/utils/validation';
|
|
5
|
+
|
|
6
|
+
export async function GET(req: NextRequest): Promise<Response> {
|
|
7
|
+
try {
|
|
8
|
+
const { searchParams } = new URL(req.url);
|
|
9
|
+
const workspaceId = searchParams.get('workspaceId');
|
|
10
|
+
|
|
11
|
+
const environments = await prisma.environment.findMany({
|
|
12
|
+
where: workspaceId ? { workspaceId } : undefined,
|
|
13
|
+
orderBy: { createdAt: 'asc' },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return success(environments) as Response;
|
|
17
|
+
} catch (err) {
|
|
18
|
+
return error(err) as Response;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
23
|
+
try {
|
|
24
|
+
const body = await req.json();
|
|
25
|
+
const parsed = CreateEnvironmentSchema.parse(body);
|
|
26
|
+
|
|
27
|
+
const environment = await prisma.environment.create({
|
|
28
|
+
data: {
|
|
29
|
+
name: parsed.name,
|
|
30
|
+
workspaceId: parsed.workspaceId,
|
|
31
|
+
variables: parsed.variables,
|
|
32
|
+
isDefault: parsed.isDefault,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return created(environment) as Response;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return error(err) as Response;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { success, error } from '@/lib/utils/api-response';
|
|
3
|
+
import { exportToOpenApi, type ExportCollection } from '@/lib/exporters/openapi';
|
|
4
|
+
|
|
5
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
6
|
+
try {
|
|
7
|
+
const body = await req.json() as {
|
|
8
|
+
collection: ExportCollection;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const spec = exportToOpenApi(body.collection);
|
|
12
|
+
|
|
13
|
+
return success({ spec, format: 'openapi-3.1' }) as Response;
|
|
14
|
+
} catch (err) {
|
|
15
|
+
return error(err) as Response;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { success, error } from '@/lib/utils/api-response';
|
|
3
|
+
import { exportToPostman } from '@/lib/exporters/postman';
|
|
4
|
+
import type { ExportCollection } from '@/lib/exporters/openapi';
|
|
5
|
+
|
|
6
|
+
export async function POST(req: NextRequest): Promise<Response> {
|
|
7
|
+
try {
|
|
8
|
+
const body = await req.json() as {
|
|
9
|
+
collection: ExportCollection;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const spec = exportToPostman(body.collection);
|
|
13
|
+
|
|
14
|
+
return success({ spec, format: 'postman-v2.1' }) as Response;
|
|
15
|
+
} catch (err) {
|
|
16
|
+
return error(err) as Response;
|
|
17
|
+
}
|
|
18
|
+
}
|