feat(frontend): request logs; refactor frontend components
This commit is contained in:
22
frontend/package-lock.json
generated
22
frontend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "mylinspirer-admin",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"solid-js": "^1.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -1423,6 +1424,27 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/class-variance-authority": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmmirror.com/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"clsx": "^2.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://polar.sh/cva"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"solid-js": "^1.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -3,10 +3,11 @@ import RulesList from './components/RulesList';
|
||||
import CommandQueue from './components/CommandQueue';
|
||||
import Login from './components/Login';
|
||||
import ChangePassword from './components/ChangePassword';
|
||||
import RequestLog from './components/RequestLog';
|
||||
import { authStore } from './api/auth';
|
||||
|
||||
const App: Component = () => {
|
||||
const [activeTab, setActiveTab] = createSignal<'rules' | 'commands'>('rules');
|
||||
const [activeTab, setActiveTab] = createSignal<'rules' | 'commands' | 'logs'>('rules');
|
||||
const [isAuthenticated, setIsAuthenticated] = createSignal(false);
|
||||
const [showChangePassword, setShowChangePassword] = createSignal(false);
|
||||
|
||||
@@ -73,6 +74,16 @@ const App: Component = () => {
|
||||
>
|
||||
Command Queue
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('logs')}
|
||||
class={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||
activeTab() === 'logs'
|
||||
? 'border-indigo-500 text-indigo-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Request Log
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -80,6 +91,7 @@ const App: Component = () => {
|
||||
<main class="max-w-7xl mx-auto px-4 py-6">
|
||||
{activeTab() === 'rules' && <RulesList />}
|
||||
{activeTab() === 'commands' && <CommandQueue />}
|
||||
{activeTab() === 'logs' && <RequestLog />}
|
||||
</main>
|
||||
|
||||
<Show when={showChangePassword()}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { InterceptionRule, Command, CreateRuleRequest, UpdateRuleRequest, UpdateCommandRequest } from '../types';
|
||||
import type { InterceptionRule, Command, CreateRuleRequest, UpdateRuleRequest, UpdateCommandRequest, RequestLog } from '../types';
|
||||
import { authStore } from './auth';
|
||||
|
||||
const API_BASE = '/admin/api';
|
||||
@@ -103,3 +103,10 @@ export const authApi = {
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
// Logs API
|
||||
export const logsApi = {
|
||||
list: () => request<RequestLog[]>('/logs'),
|
||||
};
|
||||
|
||||
export const fetchLogs = logsApi.list;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { Component, createSignal } from 'solid-js';
|
||||
import { authApi } from '../api/client';
|
||||
import { authStore } from '../api/auth';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card, CardContent, CardFooter, CardHeader } from './ui/Card';
|
||||
import { Input } from './ui/Input';
|
||||
import { Modal, ModalContent } from './ui/Modal';
|
||||
|
||||
interface ChangePasswordProps {
|
||||
onClose: () => void;
|
||||
@@ -59,13 +63,14 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<Modal>
|
||||
<ModalContent>
|
||||
<CardHeader>
|
||||
<h3 class="text-lg font-medium text-gray-900">Change Password</h3>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} class="px-6 py-4 space-y-4">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<CardContent>
|
||||
{error() && (
|
||||
<div class="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded">
|
||||
{error()}
|
||||
@@ -82,11 +87,10 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Old Password
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
value={oldPassword()}
|
||||
onInput={(e) => setOldPassword(e.currentTarget.value)}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
disabled={loading()}
|
||||
/>
|
||||
</div>
|
||||
@@ -95,11 +99,10 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
New Password
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
value={newPassword()}
|
||||
onInput={(e) => setNewPassword(e.currentTarget.value)}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
disabled={loading()}
|
||||
/>
|
||||
</div>
|
||||
@@ -108,35 +111,33 @@ const ChangePassword: Component<ChangePasswordProps> = (props) => {
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Confirm New Password
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="password"
|
||||
value={confirmPassword()}
|
||||
onInput={(e) => setConfirmPassword(e.currentTarget.value)}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
disabled={loading()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button
|
||||
</CardContent>
|
||||
<CardFooter class="flex justify-end space-x-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={props.onClose}
|
||||
class="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
disabled={loading()}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={loading()}
|
||||
>
|
||||
{loading() ? 'Changing...' : 'Change Password'}
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Component, createResource, For, Show } from 'solid-js';
|
||||
import { commandsApi } from '../api/client';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
|
||||
const CommandQueue: Component = () => {
|
||||
const [commands, { refetch }] = createResource(commandsApi.list);
|
||||
@@ -22,7 +24,7 @@ const CommandQueue: Component = () => {
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-2xl font-semibold">Command Queue</h2>
|
||||
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<Card>
|
||||
<Show when={!commands.loading} fallback={<div class="p-4">Loading...</div>}>
|
||||
<For each={commands()} fallback={
|
||||
<div class="p-8 text-center text-gray-500">
|
||||
@@ -56,18 +58,20 @@ const CommandQueue: Component = () => {
|
||||
</div>
|
||||
<Show when={cmd.status === 'unverified'}>
|
||||
<div class="ml-4 flex flex-col gap-2">
|
||||
<button
|
||||
<Button
|
||||
size="sm"
|
||||
variant="success"
|
||||
onClick={() => updateCommandStatus(cmd.id, 'verified')}
|
||||
class="px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700"
|
||||
>
|
||||
Verify
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="danger"
|
||||
onClick={() => updateCommandStatus(cmd.id, 'rejected')}
|
||||
class="px-3 py-1 bg-red-600 text-white rounded text-sm hover:bg-red-700"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
@@ -75,7 +79,7 @@ const CommandQueue: Component = () => {
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Component, createSignal } from 'solid-js';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
import { Input } from './ui/Input';
|
||||
|
||||
interface LoginProps {
|
||||
onLoginSuccess: (token: string) => void;
|
||||
@@ -39,7 +42,7 @@ const Login: Component<LoginProps> = (props) => {
|
||||
|
||||
return (
|
||||
<div class="min-h-screen bg-gray-100 flex items-center justify-center">
|
||||
<div class="bg-white p-8 rounded-lg shadow-md w-96">
|
||||
<Card class="p-8 w-96">
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-6 text-center">
|
||||
My Linspirer Admin Login
|
||||
</h1>
|
||||
@@ -49,12 +52,11 @@ const Login: Component<LoginProps> = (props) => {
|
||||
<label class="block text-gray-700 text-sm font-medium mb-2" for="password">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password()}
|
||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"
|
||||
placeholder="Enter admin password"
|
||||
disabled={isLoading()}
|
||||
required
|
||||
@@ -67,19 +69,19 @@ const Login: Component<LoginProps> = (props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
size="lg"
|
||||
disabled={isLoading()}
|
||||
class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:bg-gray-400"
|
||||
>
|
||||
{isLoading() ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<div class="mt-4 text-center text-sm text-gray-500">
|
||||
<p>Default password: admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
48
frontend/src/components/RequestDetails.tsx
Normal file
48
frontend/src/components/RequestDetails.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Component, createSignal, For } from 'solid-js';
|
||||
import type { RequestLog } from '../types';
|
||||
import { Card } from './ui/Card';
|
||||
|
||||
const TreeView: Component<{ data: any; name: string }> = (props) => {
|
||||
const [isOpen, setIsOpen] = createSignal(true);
|
||||
const isObject = typeof props.data === 'object' && props.data !== null;
|
||||
|
||||
return (
|
||||
<div class="ml-4 my-1">
|
||||
<div class="flex items-center">
|
||||
<span class="cursor-pointer" onClick={() => setIsOpen(!isOpen())}>
|
||||
{isObject ? (isOpen() ? '▼' : '►') : ''}
|
||||
</span>
|
||||
<span class="font-semibold ml-2">{props.name}</span>
|
||||
</div>
|
||||
{isOpen() && isObject && (
|
||||
<div class="pl-4 border-l-2 border-gray-200">
|
||||
<For each={Object.entries(props.data)}>
|
||||
{([key, value]) => {
|
||||
if (typeof value === 'object' && value !== null && 'modified' in value && 'value' in value) {
|
||||
return <TreeView name={key} data={value.value} />
|
||||
}
|
||||
return <TreeView name={key} data={value} />
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
{isOpen() && !isObject && <div class="pl-6 text-gray-700">{JSON.stringify(props.data)}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const RequestDetails: Component<{ log: RequestLog }> = (props) => {
|
||||
return (
|
||||
<Card class="p-4">
|
||||
<h2 class="text-xl font-bold mb-4">Request Details</h2>
|
||||
{/* TODO: interception method */}
|
||||
<div>
|
||||
<TreeView name="Request" data={props.log.request_body} />
|
||||
<TreeView name="Response" data={props.log.response_body} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestDetails;
|
||||
58
frontend/src/components/RequestLog.tsx
Normal file
58
frontend/src/components/RequestLog.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Component, createSignal, onMount, For, Show } from 'solid-js';
|
||||
import { fetchLogs } from '../api/client';
|
||||
import type { RequestLog as RequestLogType } from '../types';
|
||||
import RequestDetails from './RequestDetails';
|
||||
import { Card } from './ui/Card';
|
||||
|
||||
const RequestLog: Component = () => {
|
||||
const [logs, setLogs] = createSignal<RequestLogType[]>([]);
|
||||
const [selectedLog, setSelectedLog] = createSignal<RequestLogType | null>(null);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const fetchedLogs = await fetchLogs();
|
||||
setLogs(fetchedLogs);
|
||||
} catch (error) {
|
||||
console.error('Error fetching logs:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const handleLogClick = (log: RequestLogType) => {
|
||||
setSelectedLog(log);
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex space-x-4">
|
||||
<div class={selectedLog() ? "w-1/3" : "w-full"}>
|
||||
<h2 class="text-2xl font-semibold">Request Log</h2>
|
||||
<div class="overflow-x-auto">
|
||||
<Card>
|
||||
<ul class="divide-y divide-gray-200">
|
||||
<For each={logs()}>
|
||||
{(log) => (
|
||||
<li
|
||||
class="p-4 hover:bg-gray-50 cursor-pointer"
|
||||
onClick={() => handleLogClick(log)}
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<span class="font-medium">{log.method}</span>
|
||||
<span class="text-sm text-gray-500">{new Date(log.created_at).toLocaleTimeString()}</span>
|
||||
</div>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={selectedLog()}>
|
||||
{(log) =>
|
||||
<div class="w-2/3">
|
||||
<RequestDetails log={log()} />
|
||||
</div>}
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RequestLog;
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Component, createSignal, createResource, For, Show } from 'solid-js';
|
||||
import { rulesApi } from '../api/client';
|
||||
import type { InterceptionRule } from '../types';
|
||||
import { Button } from './ui/Button';
|
||||
import { Card } from './ui/Card';
|
||||
import { Input } from './ui/Input';
|
||||
import { Select } from './ui/Select';
|
||||
import { Textarea } from './ui/Textarea';
|
||||
|
||||
const RulesList: Component = () => {
|
||||
const [rules, { refetch }] = createResource(rulesApi.list);
|
||||
@@ -105,7 +110,7 @@ const RulesList: Component = () => {
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-2xl font-semibold">Interception Rules</h2>
|
||||
<button
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (showEditor()) {
|
||||
cancelEdit();
|
||||
@@ -113,14 +118,13 @@ const RulesList: Component = () => {
|
||||
setShowEditor(true);
|
||||
}
|
||||
}}
|
||||
class="bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700"
|
||||
>
|
||||
{showEditor() ? 'Cancel' : '+ New Rule'}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Show when={showEditor()}>
|
||||
<div class="bg-white shadow rounded-lg p-6 mb-4">
|
||||
<Card class="p-6 mb-4">
|
||||
<h3 class="text-lg font-semibold mb-4">
|
||||
{editingId() !== null ? 'Edit Rule' : 'Create New Rule'}
|
||||
</h3>
|
||||
@@ -129,11 +133,10 @@ const RulesList: Component = () => {
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Method Name
|
||||
</label>
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={editingMethod()}
|
||||
onInput={(e) => setEditingMethod(e.currentTarget.value)}
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
placeholder="com.linspirer.method.name"
|
||||
/>
|
||||
</div>
|
||||
@@ -142,14 +145,13 @@ const RulesList: Component = () => {
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Action
|
||||
</label>
|
||||
<select
|
||||
<Select
|
||||
value={editingAction()}
|
||||
onChange={(e) => setEditingAction(e.currentTarget.value as any)}
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2"
|
||||
>
|
||||
<option value="passthrough">Passthrough (Forward to server)</option>
|
||||
<option value="replace">Replace (Custom response)</option>
|
||||
</select>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Show when={editingAction() === 'replace'}>
|
||||
@@ -157,27 +159,25 @@ const RulesList: Component = () => {
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Custom Response (JSON)
|
||||
</label>
|
||||
<textarea
|
||||
<Textarea
|
||||
value={editingResponse()}
|
||||
onInput={(e) => setEditingResponse(e.currentTarget.value)}
|
||||
rows={10}
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-2 font-mono text-sm"
|
||||
placeholder='{"code": 0, "type": "object", "data": {...}}'
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<button
|
||||
<Button
|
||||
onClick={saveEdit}
|
||||
class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700"
|
||||
>
|
||||
{editingId() !== null ? 'Update Rule' : 'Create Rule'}
|
||||
</button>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<Card>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
@@ -225,16 +225,18 @@ const RulesList: Component = () => {
|
||||
</button>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => startEdit(rule)}
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-4">
|
||||
class="mr-4">
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => deleteRule(rule.id)}
|
||||
class="text-red-600 hover:text-red-900">
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
@@ -243,7 +245,7 @@ const RulesList: Component = () => {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
41
frontend/src/components/ui/Button.tsx
Normal file
41
frontend/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'bg-indigo-600 text-white hover:bg-indigo-700',
|
||||
secondary: 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50',
|
||||
danger: 'bg-red-600 text-white hover:bg-red-700',
|
||||
success: 'bg-green-600 text-white hover:bg-green-700',
|
||||
ghost: 'text-indigo-600 hover:text-indigo-900',
|
||||
link: 'text-red-600 hover:text-red-900',
|
||||
},
|
||||
size: {
|
||||
sm: 'px-3 py-1',
|
||||
md: 'px-4 py-2',
|
||||
lg: 'w-full px-4 py-2',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button: Component<ButtonProps> = (props) => {
|
||||
const [local, others] = splitProps(props, ['variant', 'size', 'class']);
|
||||
return (
|
||||
<button
|
||||
class={buttonVariants({ variant: local.variant, size: local.size, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Button, buttonVariants };
|
||||
59
frontend/src/components/ui/Card.tsx
Normal file
59
frontend/src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const cardVariants = cva('bg-white shadow rounded-lg', {
|
||||
variants: {
|
||||
variant: {
|
||||
primary: 'overflow-hidden',
|
||||
withPadding: 'p-6',
|
||||
withPaddingAndDivider: 'p-4 divide-y divide-gray-200',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
});
|
||||
|
||||
export interface CardProps extends JSX.HTMLAttributes<HTMLDivElement>, VariantProps<typeof cardVariants> {}
|
||||
|
||||
const Card: Component<CardProps> = (props) => {
|
||||
const [local, others] = splitProps(props, ['variant', 'class']);
|
||||
return (
|
||||
<div
|
||||
class={cardVariants({ variant: local.variant, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CardHeader: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`px-6 py-4 border-b border-gray-200 ${local.class}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CardContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`px-6 py-4 space-y-4 ${local.class}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const CardFooter: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`px-6 py-4 border-t border-gray-200 ${local.class}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Card, CardHeader, CardContent, CardFooter, cardVariants };
|
||||
30
frontend/src/components/ui/Input.tsx
Normal file
30
frontend/src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const inputVariants = cva(
|
||||
'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface InputProps extends JSX.InputHTMLAttributes<HTMLInputElement>, VariantProps<typeof inputVariants> {}
|
||||
|
||||
const Input: Component<InputProps> = (props) => {
|
||||
const [local, others] = splitProps(props, ['variant', 'class']);
|
||||
return (
|
||||
<input
|
||||
class={inputVariants({ variant: local.variant, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Input, inputVariants };
|
||||
23
frontend/src/components/ui/Modal.tsx
Normal file
23
frontend/src/components/ui/Modal.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
|
||||
const Modal: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`fixed inset-0 bg-gray-500 bg-opacity-75 flex items-center justify-center z-50 ${local.class}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ModalContent: Component<JSX.HTMLAttributes<HTMLDivElement>> = (props) => {
|
||||
const [local, others] = splitProps(props, ['class']);
|
||||
return (
|
||||
<div
|
||||
class={`bg-white rounded-lg shadow-xl max-w-md w-full mx-4 ${local.class}`}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Modal, ModalContent };
|
||||
30
frontend/src/components/ui/Select.tsx
Normal file
30
frontend/src/components/ui/Select.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const selectVariants = cva(
|
||||
'w-full border border-gray-300 rounded-md px-3 py-2',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface SelectProps extends JSX.SelectHTMLAttributes<HTMLSelectElement>, VariantProps<typeof selectVariants> {}
|
||||
|
||||
const Select: Component<SelectProps> = (props) => {
|
||||
const [local, others] = splitProps(props, ['variant', 'class']);
|
||||
return (
|
||||
<select
|
||||
class={selectVariants({ variant: local.variant, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Select, selectVariants };
|
||||
30
frontend/src/components/ui/Textarea.tsx
Normal file
30
frontend/src/components/ui/Textarea.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Component, JSX, splitProps } from 'solid-js';
|
||||
import { cva, type VariantProps } from 'class-variance-authority';
|
||||
|
||||
const textareaVariants = cva(
|
||||
'w-full border border-gray-300 rounded-md px-3 py-2 font-mono text-sm',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
primary: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'primary',
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export interface TextareaProps extends JSX.TextareaHTMLAttributes<HTMLTextAreaElement>, VariantProps<typeof textareaVariants> {}
|
||||
|
||||
const Textarea: Component<TextareaProps> = (props) => {
|
||||
const [local, others] = splitProps(props, ['variant', 'class']);
|
||||
return (
|
||||
<textarea
|
||||
class={textareaVariants({ variant: local.variant, class: local.class })}
|
||||
{...others}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Textarea, textareaVariants };
|
||||
@@ -34,3 +34,23 @@ export interface UpdateCommandRequest {
|
||||
status: string;
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export interface Request {
|
||||
headers: Record<string, {value: string, modified: boolean}>;
|
||||
body: {value: any, modified: boolean};
|
||||
}
|
||||
|
||||
export interface Response {
|
||||
headers: Record<string, {value: string, modified: boolean}>;
|
||||
body: {value: any, modified: boolean};
|
||||
}
|
||||
|
||||
export interface RequestLog {
|
||||
id: number;
|
||||
method: string;
|
||||
request_body: object;
|
||||
response_body: object;
|
||||
request_interception_action: string,
|
||||
response_interception_action: string,
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -39,21 +39,8 @@ CREATE TABLE IF NOT EXISTS commands (
|
||||
notes TEXT
|
||||
);
|
||||
|
||||
-- Request/Response logs (optional, can be large)
|
||||
CREATE TABLE IF NOT EXISTS request_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
method TEXT,
|
||||
path TEXT,
|
||||
request_body TEXT,
|
||||
response_body TEXT,
|
||||
status_code INTEGER,
|
||||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
-- Index for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_commands_status ON commands(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON request_logs(timestamp);
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_method ON request_logs(method);
|
||||
|
||||
-- Insert default config values
|
||||
INSERT OR IGNORE INTO config (key, value, description) VALUES
|
||||
|
||||
14
migrations/20251201181400_add_request_logs.sql
Normal file
14
migrations/20251201181400_add_request_logs.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Add request logs
|
||||
CREATE TABLE request_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
method TEXT,
|
||||
request_body TEXT,
|
||||
response_body TEXT,
|
||||
request_interception_action TEXT,
|
||||
response_interception_action TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Indexes for performance
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_created_at ON request_logs(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_logs_method ON request_logs(method);
|
||||
@@ -7,7 +7,10 @@ use axum::{
|
||||
};
|
||||
use tracing::error;
|
||||
|
||||
use crate::{AppState, auth, crypto, db};
|
||||
use crate::{
|
||||
AppState, auth,
|
||||
db::{self, models::RequestLog},
|
||||
};
|
||||
|
||||
use super::models::*;
|
||||
|
||||
@@ -348,3 +351,19 @@ pub async fn update_config(
|
||||
|
||||
Ok(StatusCode::OK)
|
||||
}
|
||||
|
||||
// Log handlers
|
||||
pub async fn list_logs(
|
||||
State(state): State<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<RequestLog>>, (StatusCode, Json<ApiError>)> {
|
||||
match db::repositories::logs::list_all(&state.db).await {
|
||||
Ok(logs) => Ok(Json(logs)),
|
||||
Err(e) => {
|
||||
error!("Failed to list logs: {}", e);
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(ApiError::new("Failed to fetch logs")),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,3 +99,21 @@ impl ApiError {
|
||||
Self { error: msg.into() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RequestDetails {
|
||||
pub headers: Vec<Header>,
|
||||
pub body: Body,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Body {
|
||||
pub value: Value,
|
||||
pub modified: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Header {
|
||||
pub value: String,
|
||||
pub modified: bool,
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ pub fn admin_routes(state: Arc<AppState>) -> Router<Arc<AppState>> {
|
||||
.route("/api/commands/{:id}", post(handlers::verify_command))
|
||||
.route("/api/config", get(handlers::get_config))
|
||||
.route("/api/config", put(handlers::update_config))
|
||||
.route("/api/logs", get(handlers::list_logs))
|
||||
.layer(axum::middleware::from_fn_with_state(
|
||||
state,
|
||||
auth_middleware::auth_middleware,
|
||||
|
||||
@@ -2,12 +2,25 @@ pub mod models;
|
||||
pub mod repositories;
|
||||
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions};
|
||||
use std::str::FromStr;
|
||||
use tracing::info;
|
||||
use std::{path::Path, str::FromStr};
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub async fn init_db(database_url: &str) -> anyhow::Result<SqlitePool> {
|
||||
info!("Initializing database at: {}", database_url);
|
||||
|
||||
// Create directories
|
||||
{
|
||||
// remove scheme from the URL
|
||||
let url = database_url
|
||||
.trim_start_matches("sqlite://")
|
||||
.trim_start_matches("sqlite:");
|
||||
let path = url.split('?').next().unwrap_or_default();
|
||||
if let Some(dir) = Path::new(path).parent()
|
||||
&& let Err(err) = tokio::fs::create_dir_all(dir).await
|
||||
{
|
||||
warn!("Failed to create directories for database ({database_url}): {err}")
|
||||
}
|
||||
}
|
||||
// Parse connection options
|
||||
let options = SqliteConnectOptions::from_str(database_url)?.create_if_missing(true);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct InterceptionRule {
|
||||
@@ -29,23 +30,13 @@ pub struct Config {
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct ResponseTemplate {
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
pub method_name: String,
|
||||
pub response_json: String,
|
||||
pub description: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct RequestLog {
|
||||
pub id: i64,
|
||||
pub method: Option<String>,
|
||||
pub path: String,
|
||||
pub request_body: String,
|
||||
pub response_body: String,
|
||||
pub status_code: i32,
|
||||
pub timestamp: String,
|
||||
pub method: String,
|
||||
pub request_body: Value,
|
||||
pub response_body: Value,
|
||||
pub request_interception_action: String,
|
||||
pub response_interception_action: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
39
src/db/repositories/logs.rs
Normal file
39
src/db/repositories/logs.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::db::models::RequestLog;
|
||||
use serde_json::Value;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
pub async fn list_all(pool: &SqlitePool) -> anyhow::Result<Vec<RequestLog>> {
|
||||
Ok(
|
||||
sqlx::query_as::<_, RequestLog>("SELECT * FROM request_logs ORDER BY created_at")
|
||||
.fetch_all(pool)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
pool: &SqlitePool,
|
||||
method: String,
|
||||
request_body: Value,
|
||||
response_body: Value,
|
||||
request_interception_action: String,
|
||||
response_interception_action: String,
|
||||
) -> Result<i64, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO request_logs (
|
||||
method,
|
||||
request_body,
|
||||
response_body,
|
||||
request_interception_action,
|
||||
response_interception_action
|
||||
) VALUES (?, ?, ?, ?, ?)",
|
||||
)
|
||||
.bind(method)
|
||||
.bind(request_body)
|
||||
.bind(response_body)
|
||||
.bind(request_interception_action)
|
||||
.bind(response_interception_action)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
Ok(result.last_insert_rowid())
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod commands;
|
||||
pub mod config;
|
||||
pub mod logs;
|
||||
pub mod rules;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{net::SocketAddr, sync::Arc};
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{Router, routing::any};
|
||||
use axum::handler::Handler;
|
||||
use axum::{Router, routing::any};
|
||||
use dotenvy::dotenv;
|
||||
use tower_http::compression::CompressionLayer;
|
||||
use tracing::{error, info, level_filters::LevelFilter};
|
||||
@@ -88,7 +88,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
// Admin routes
|
||||
.nest("/admin", admin::routes::admin_routes(state.clone()))
|
||||
// Proxy Linspirer APIs
|
||||
.route("/public-interface.php", any(proxy::proxy_handler.layer(proxy_middleware)))
|
||||
.route(
|
||||
"/public-interface.php",
|
||||
any(proxy::proxy_handler.layer(proxy_middleware)),
|
||||
)
|
||||
.layer(CompressionLayer::new().gzip(true))
|
||||
.with_state(state);
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::str;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::{
|
||||
extract::{OriginalUri, State},
|
||||
extract::State,
|
||||
http::{Request, StatusCode},
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
@@ -11,7 +11,7 @@ use http_body_util::BodyExt;
|
||||
use serde_json::Value;
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::{AppState, crypto};
|
||||
use crate::{AppState, crypto, db};
|
||||
|
||||
enum ResponseBody {
|
||||
Original(String),
|
||||
@@ -20,7 +20,6 @@ enum ResponseBody {
|
||||
|
||||
pub async fn middleware(
|
||||
State(state): State<Arc<AppState>>,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
req: Request<axum::body::Body>,
|
||||
next: Next,
|
||||
) -> impl IntoResponse {
|
||||
@@ -37,9 +36,7 @@ pub async fn middleware(
|
||||
}
|
||||
};
|
||||
|
||||
let path = uri.path();
|
||||
|
||||
let (decrypted_request_for_log, method) = match str::from_utf8(&body_bytes)
|
||||
let (decrypted_request, method) = match str::from_utf8(&body_bytes)
|
||||
.map_err(anyhow::Error::from)
|
||||
.and_then(|body| process_and_log_request(body, &state.key, &state.iv))
|
||||
{
|
||||
@@ -78,9 +75,8 @@ pub async fn middleware(
|
||||
let resp_body_text = String::from_utf8(body_bytes.clone().to_vec()).unwrap_or_default();
|
||||
|
||||
// Check for generic method interception first
|
||||
let response_body_to_log = if let Some(method_str) = &method {
|
||||
if let Ok(Some(intercepted)) =
|
||||
maybe_intercept_response(method_str, &resp_body_text, &state).await
|
||||
let response_body = if let Some(method_str) = &method {
|
||||
if let Ok(Some(intercepted)) = intercept_response(method_str, &resp_body_text, &state).await
|
||||
{
|
||||
info!("Intercepting response for method: {}", method_str);
|
||||
ResponseBody::Original(intercepted)
|
||||
@@ -108,28 +104,46 @@ pub async fn middleware(
|
||||
ResponseBody::Original(resp_body_text.clone())
|
||||
};
|
||||
|
||||
let (decrypted_response_for_log, final_response_body) = match response_body_to_log {
|
||||
let (decrypted_response, final_response_body) = match response_body {
|
||||
ResponseBody::Original(body_text) => {
|
||||
let decrypted = decrypt_and_format(&body_text, &state.key, &state.iv)
|
||||
.unwrap_or_else(|_| "Could not decrypt or format response".to_string());
|
||||
let decrypted =
|
||||
decrypt_and_format(&body_text, &state.key, &state.iv).unwrap_or_else(|_| {
|
||||
Value::String("Could not decrypt or format response".to_string())
|
||||
});
|
||||
(decrypted, body_text)
|
||||
}
|
||||
ResponseBody::Modified(body_value) => {
|
||||
let pretty_printed = serde_json::to_string_pretty(&body_value).unwrap_or_default();
|
||||
ResponseBody::Modified(response_body_value) => {
|
||||
let pretty_printed =
|
||||
serde_json::to_string_pretty(&response_body_value).unwrap_or_default();
|
||||
let encrypted = crypto::encrypt(&pretty_printed, &state.key, &state.iv)
|
||||
.unwrap_or_else(|_| "Failed to encrypt modified response".to_string());
|
||||
(pretty_printed, encrypted)
|
||||
(response_body_value, encrypted)
|
||||
}
|
||||
};
|
||||
|
||||
debug!(
|
||||
"{}\nRequest:\n{}\nResponse:\n{}\n{}",
|
||||
path,
|
||||
serde_json::to_string_pretty(&decrypted_request_for_log).unwrap_or_default(),
|
||||
decrypted_response_for_log,
|
||||
"\nRequest:\n{}\nResponse:\n{}\n{}",
|
||||
serde_json::to_string_pretty(&decrypted_request).unwrap_or_default(),
|
||||
serde_json::to_string_pretty(&decrypted_response).unwrap_or_default(),
|
||||
"-".repeat(80),
|
||||
);
|
||||
|
||||
if let Some(method) = method {
|
||||
// TODO: interception action
|
||||
if let Err(e) = db::repositories::logs::create(
|
||||
&state.db,
|
||||
method,
|
||||
decrypted_request,
|
||||
decrypted_response,
|
||||
"".into(),
|
||||
"".into(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to log request: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let mut response_builder = Response::builder().status(resp_parts.status);
|
||||
if !resp_parts.headers.is_empty() {
|
||||
*response_builder.headers_mut().unwrap() = resp_parts.headers;
|
||||
@@ -220,13 +234,12 @@ async fn handle_getcommand_response(
|
||||
Ok(response_json)
|
||||
}
|
||||
|
||||
fn decrypt_and_format(body_text: &str, key: &str, iv: &str) -> anyhow::Result<String> {
|
||||
fn decrypt_and_format(body_text: &str, key: &str, iv: &str) -> anyhow::Result<Value> {
|
||||
let decrypted = crypto::decrypt(body_text, key, iv)?;
|
||||
let formatted: Value = serde_json::from_str(&decrypted)?;
|
||||
Ok(serde_json::to_string_pretty(&formatted)?)
|
||||
Ok(serde_json::from_str(&decrypted)?)
|
||||
}
|
||||
|
||||
async fn maybe_intercept_response(
|
||||
async fn intercept_response(
|
||||
method: &str,
|
||||
_original_response: &str,
|
||||
state: &Arc<AppState>,
|
||||
|
||||
Reference in New Issue
Block a user