mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-10 19:16:55 +02:00
Refactor default headers to be injected dynamically (#367)
This commit is contained in:
@@ -4,6 +4,8 @@ use std::sync::Arc;
|
|||||||
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
|
use tauri::{AppHandle, Manager, Runtime, State, WebviewWindow, command};
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
use tauri_plugin_dialog::{DialogExt, MessageDialogKind};
|
||||||
use yaak_crypto::manager::EncryptionManager;
|
use yaak_crypto::manager::EncryptionManager;
|
||||||
|
use yaak_models::models::HttpRequestHeader;
|
||||||
|
use yaak_models::queries::workspaces::default_headers;
|
||||||
use yaak_plugins::events::GetThemesResponse;
|
use yaak_plugins::events::GetThemesResponse;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
use yaak_plugins::native_template_functions::{
|
use yaak_plugins::native_template_functions::{
|
||||||
@@ -97,3 +99,8 @@ pub(crate) async fn cmd_set_workspace_key<R: Runtime>(
|
|||||||
window.crypto().set_human_key(workspace_id, key)?;
|
window.crypto().set_human_key(workspace_id, key)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[command]
|
||||||
|
pub(crate) fn cmd_default_headers() -> Vec<HttpRequestHeader> {
|
||||||
|
default_headers()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1718,6 +1718,7 @@ pub fn run() {
|
|||||||
//
|
//
|
||||||
// Migrated commands
|
// Migrated commands
|
||||||
crate::commands::cmd_decrypt_template,
|
crate::commands::cmd_decrypt_template,
|
||||||
|
crate::commands::cmd_default_headers,
|
||||||
crate::commands::cmd_enable_encryption,
|
crate::commands::cmd_enable_encryption,
|
||||||
crate::commands::cmd_get_themes,
|
crate::commands::cmd_get_themes,
|
||||||
crate::commands::cmd_reveal_workspace_key,
|
crate::commands::cmd_reveal_workspace_key,
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
-- Filter out headers that match the hardcoded defaults (User-Agent: yaak, Accept: */*),
|
||||||
|
-- keeping any other custom headers the user may have added.
|
||||||
|
UPDATE workspaces
|
||||||
|
SET headers = (
|
||||||
|
SELECT json_group_array(json(value))
|
||||||
|
FROM json_each(headers)
|
||||||
|
WHERE NOT (
|
||||||
|
(LOWER(json_extract(value, '$.name')) = 'user-agent' AND json_extract(value, '$.value') = 'yaak')
|
||||||
|
OR (LOWER(json_extract(value, '$.name')) = 'accept' AND json_extract(value, '$.value') = '*/*')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
WHERE json_array_length(headers) > 0;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::dedupe_headers;
|
||||||
use crate::db_context::DbContext;
|
use crate::db_context::DbContext;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{GrpcRequest, GrpcRequestIden, HttpRequestHeader};
|
use crate::models::{GrpcRequest, GrpcRequestIden, HttpRequestHeader};
|
||||||
@@ -87,6 +88,6 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
metadata.append(&mut grpc_request.metadata.clone());
|
metadata.append(&mut grpc_request.metadata.clone());
|
||||||
|
|
||||||
Ok(metadata)
|
Ok(dedupe_headers(metadata))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::dedupe_headers;
|
||||||
use crate::db_context::DbContext;
|
use crate::db_context::DbContext;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
|
use crate::models::{Folder, FolderIden, HttpRequest, HttpRequestHeader, HttpRequestIden};
|
||||||
@@ -87,7 +88,7 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
headers.append(&mut http_request.headers.clone());
|
headers.append(&mut http_request.headers.clone());
|
||||||
|
|
||||||
Ok(headers)
|
Ok(dedupe_headers(headers))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_http_requests_for_folder_recursive(
|
pub fn list_http_requests_for_folder_recursive(
|
||||||
|
|||||||
@@ -19,6 +19,26 @@ mod websocket_connections;
|
|||||||
mod websocket_events;
|
mod websocket_events;
|
||||||
mod websocket_requests;
|
mod websocket_requests;
|
||||||
mod workspace_metas;
|
mod workspace_metas;
|
||||||
mod workspaces;
|
pub mod workspaces;
|
||||||
|
|
||||||
const MAX_HISTORY_ITEMS: usize = 20;
|
const MAX_HISTORY_ITEMS: usize = 20;
|
||||||
|
|
||||||
|
use crate::models::HttpRequestHeader;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
/// Deduplicate headers by name (case-insensitive), keeping the latest (most specific) value.
|
||||||
|
/// Preserves the order of first occurrence for each header name.
|
||||||
|
pub(crate) fn dedupe_headers(headers: Vec<HttpRequestHeader>) -> Vec<HttpRequestHeader> {
|
||||||
|
let mut index_by_name: HashMap<String, usize> = HashMap::new();
|
||||||
|
let mut deduped: Vec<HttpRequestHeader> = Vec::new();
|
||||||
|
for header in headers {
|
||||||
|
let key = header.name.to_lowercase();
|
||||||
|
if let Some(&idx) = index_by_name.get(&key) {
|
||||||
|
deduped[idx] = header;
|
||||||
|
} else {
|
||||||
|
index_by_name.insert(key, deduped.len());
|
||||||
|
deduped.push(header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deduped
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use super::dedupe_headers;
|
||||||
use crate::db_context::DbContext;
|
use crate::db_context::DbContext;
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use crate::models::{HttpRequestHeader, WebsocketRequest, WebsocketRequestIden};
|
use crate::models::{HttpRequestHeader, WebsocketRequest, WebsocketRequestIden};
|
||||||
@@ -95,6 +96,6 @@ impl<'a> DbContext<'a> {
|
|||||||
|
|
||||||
headers.append(&mut websocket_request.headers.clone());
|
headers.append(&mut websocket_request.headers.clone());
|
||||||
|
|
||||||
Ok(headers)
|
Ok(dedupe_headers(headers))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,28 +65,7 @@ impl<'a> DbContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
|
pub fn upsert_workspace(&self, w: &Workspace, source: &UpdateSource) -> Result<Workspace> {
|
||||||
let mut workspace = w.clone();
|
self.upsert(w, source)
|
||||||
|
|
||||||
// Add default headers only for NEW workspaces (empty ID means insert, not update)
|
|
||||||
// This prevents re-adding headers if a user intentionally removes all headers
|
|
||||||
if workspace.id.is_empty() && workspace.headers.is_empty() {
|
|
||||||
workspace.headers = vec![
|
|
||||||
HttpRequestHeader {
|
|
||||||
enabled: true,
|
|
||||||
name: "User-Agent".to_string(),
|
|
||||||
value: "yaak".to_string(),
|
|
||||||
id: None,
|
|
||||||
},
|
|
||||||
HttpRequestHeader {
|
|
||||||
enabled: true,
|
|
||||||
name: "Accept".to_string(),
|
|
||||||
value: "*/*".to_string(),
|
|
||||||
id: None,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
self.upsert(&workspace, source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_auth_for_workspace(
|
pub fn resolve_auth_for_workspace(
|
||||||
@@ -101,6 +80,28 @@ impl<'a> DbContext<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
|
pub fn resolve_headers_for_workspace(&self, workspace: &Workspace) -> Vec<HttpRequestHeader> {
|
||||||
workspace.headers.clone()
|
let mut headers = default_headers();
|
||||||
|
headers.extend(workspace.headers.clone());
|
||||||
|
headers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Global default headers that are always sent with requests unless overridden.
|
||||||
|
/// These are prepended to the inheritance chain so workspace/folder/request headers
|
||||||
|
/// can override or disable them.
|
||||||
|
pub fn default_headers() -> Vec<HttpRequestHeader> {
|
||||||
|
vec![
|
||||||
|
HttpRequestHeader {
|
||||||
|
enabled: true,
|
||||||
|
name: "User-Agent".to_string(),
|
||||||
|
value: "yaak".to_string(),
|
||||||
|
id: None,
|
||||||
|
},
|
||||||
|
HttpRequestHeader {
|
||||||
|
enabled: true,
|
||||||
|
name: "Accept".to_string(),
|
||||||
|
value: "*/*".to_string(),
|
||||||
|
id: None,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Props = {
|
|||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
headers: HttpRequestHeader[];
|
headers: HttpRequestHeader[];
|
||||||
inheritedHeaders?: HttpRequestHeader[];
|
inheritedHeaders?: HttpRequestHeader[];
|
||||||
|
inheritedHeadersLabel?: string;
|
||||||
stateKey: string;
|
stateKey: string;
|
||||||
onChange: (headers: HttpRequestHeader[]) => void;
|
onChange: (headers: HttpRequestHeader[]) => void;
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -28,11 +29,20 @@ export function HeadersEditor({
|
|||||||
stateKey,
|
stateKey,
|
||||||
headers,
|
headers,
|
||||||
inheritedHeaders,
|
inheritedHeaders,
|
||||||
|
inheritedHeadersLabel = 'Inherited',
|
||||||
onChange,
|
onChange,
|
||||||
forceUpdateKey,
|
forceUpdateKey,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
// Get header names defined at current level (case-insensitive)
|
||||||
|
const currentHeaderNames = new Set(
|
||||||
|
headers.filter((h) => h.name).map((h) => h.name.toLowerCase()),
|
||||||
|
);
|
||||||
|
// Filter inherited headers: must be enabled, have content, and not be overridden by current level
|
||||||
const validInheritedHeaders =
|
const validInheritedHeaders =
|
||||||
inheritedHeaders?.filter((pair) => pair.enabled && (pair.name || pair.value)) ?? [];
|
inheritedHeaders?.filter(
|
||||||
|
(pair) =>
|
||||||
|
pair.enabled && (pair.name || pair.value) && !currentHeaderNames.has(pair.name.toLowerCase()),
|
||||||
|
) ?? [];
|
||||||
const hasInheritedHeaders = validInheritedHeaders.length > 0;
|
const hasInheritedHeaders = validInheritedHeaders.length > 0;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -48,7 +58,7 @@ export function HeadersEditor({
|
|||||||
className="text-sm"
|
className="text-sm"
|
||||||
summary={
|
summary={
|
||||||
<HStack>
|
<HStack>
|
||||||
Inherited <CountBadge count={validInheritedHeaders.length} />
|
{inheritedHeadersLabel} <CountBadge count={validInheritedHeaders.length} />
|
||||||
</HStack>
|
</HStack>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
<TabContent value={TAB_HEADERS} className="overflow-y-auto h-full px-4">
|
<TabContent value={TAB_HEADERS} className="overflow-y-auto h-full px-4">
|
||||||
<HeadersEditor
|
<HeadersEditor
|
||||||
inheritedHeaders={inheritedHeaders}
|
inheritedHeaders={inheritedHeaders}
|
||||||
|
inheritedHeadersLabel="Defaults"
|
||||||
forceUpdateKey={workspace.id}
|
forceUpdateKey={workspace.id}
|
||||||
headers={workspace.headers}
|
headers={workspace.headers}
|
||||||
onChange={(headers) => patchModel(workspace, { headers })}
|
onChange={(headers) => patchModel(workspace, { headers })}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
} from '@yaakapp-internal/models';
|
} from '@yaakapp-internal/models';
|
||||||
import { foldersAtom, workspacesAtom } from '@yaakapp-internal/models';
|
import { foldersAtom, workspacesAtom } from '@yaakapp-internal/models';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
|
import { defaultHeaders } from '../lib/defaultHeaders';
|
||||||
|
|
||||||
const ancestorsAtom = atom((get) => [...get(foldersAtom), ...get(workspacesAtom)]);
|
const ancestorsAtom = atom((get) => [...get(foldersAtom), ...get(workspacesAtom)]);
|
||||||
|
|
||||||
@@ -17,12 +18,12 @@ export function useInheritedHeaders(baseModel: HeaderModel | null) {
|
|||||||
const parents = useAtomValue(ancestorsAtom);
|
const parents = useAtomValue(ancestorsAtom);
|
||||||
|
|
||||||
if (baseModel == null) return [];
|
if (baseModel == null) return [];
|
||||||
if (baseModel.model === 'workspace') return [];
|
if (baseModel.model === 'workspace') return defaultHeaders;
|
||||||
|
|
||||||
const next = (child: HeaderModel): HttpRequestHeader[] => {
|
const next = (child: HeaderModel): HttpRequestHeader[] => {
|
||||||
// Short-circuit
|
// Short-circuit at workspace level - return global defaults + workspace headers
|
||||||
if (child.model === 'workspace') {
|
if (child.model === 'workspace') {
|
||||||
return [];
|
return [...defaultHeaders, ...child.headers];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recurse up the tree
|
// Recurse up the tree
|
||||||
@@ -40,5 +41,13 @@ export function useInheritedHeaders(baseModel: HeaderModel | null) {
|
|||||||
return [...headers, ...parent.headers];
|
return [...headers, ...parent.headers];
|
||||||
};
|
};
|
||||||
|
|
||||||
return next(baseModel);
|
const allHeaders = next(baseModel);
|
||||||
|
|
||||||
|
// Deduplicate by header name (case-insensitive), keeping the latest (most specific) value
|
||||||
|
const headersByName = new Map<string, HttpRequestHeader>();
|
||||||
|
for (const header of allHeaders) {
|
||||||
|
headersByName.set(header.name.toLowerCase(), header);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(headersByName.values());
|
||||||
}
|
}
|
||||||
|
|||||||
8
src-web/lib/defaultHeaders.ts
Normal file
8
src-web/lib/defaultHeaders.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import type { HttpRequestHeader } from '@yaakapp-internal/models';
|
||||||
|
import { invokeCmd } from './tauri';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Global default headers fetched from the backend.
|
||||||
|
* These are static and fetched once on module load.
|
||||||
|
*/
|
||||||
|
export const defaultHeaders: HttpRequestHeader[] = await invokeCmd('cmd_default_headers');
|
||||||
@@ -12,6 +12,7 @@ type TauriCmd =
|
|||||||
| 'cmd_create_grpc_request'
|
| 'cmd_create_grpc_request'
|
||||||
| 'cmd_curl_to_request'
|
| 'cmd_curl_to_request'
|
||||||
| 'cmd_decrypt_template'
|
| 'cmd_decrypt_template'
|
||||||
|
| 'cmd_default_headers'
|
||||||
| 'cmd_delete_all_grpc_connections'
|
| 'cmd_delete_all_grpc_connections'
|
||||||
| 'cmd_delete_all_http_responses'
|
| 'cmd_delete_all_http_responses'
|
||||||
| 'cmd_delete_send_history'
|
| 'cmd_delete_send_history'
|
||||||
|
|||||||
Reference in New Issue
Block a user