This commit is contained in:
Julian Freeman
2025-12-01 08:41:44 -04:00
parent c7c7b5fc4b
commit ee586ae06f
5 changed files with 317 additions and 2 deletions

View File

@@ -0,0 +1,153 @@
<script setup lang="ts">
import { useRequestStore } from '../stores/requestStore';
import { ShieldCheck, Key, User, Lock, Fingerprint } from 'lucide-vue-next';
import { ref } from 'vue';
const store = useRequestStore();
const authTypes = [
{ value: 'none', label: 'No Auth' },
{ value: 'basic', label: 'Basic Auth' },
{ value: 'bearer', label: 'Bearer Token' },
{ value: 'api_key', label: 'API Key' },
];
const showPassword = ref(false);
const showApiKey = ref(false);
</script>
<template>
<div class="h-full p-4 flex flex-col gap-6 overflow-y-auto text-slate-300">
<!-- Type Selector -->
<div class="flex flex-col gap-2">
<label class="text-xs font-bold uppercase text-slate-500 tracking-wider">Authentication Type</label>
<div class="relative">
<select
v-model="store.activeRequest.auth.type"
class="w-full bg-slate-950 border border-slate-800 rounded-lg px-4 py-2.5 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none appearance-none cursor-pointer"
>
<option v-for="t in authTypes" :key="t.value" :value="t.value">{{ t.label }}</option>
</select>
<ShieldCheck class="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" />
</div>
</div>
<!-- Dynamic Content -->
<div class="flex-1">
<!-- No Auth -->
<div v-if="store.activeRequest.auth.type === 'none'" class="flex flex-col items-center justify-center h-40 text-slate-600 gap-3">
<ShieldCheck class="w-12 h-12 opacity-20" />
<p class="text-sm">This request does not use any authentication.</p>
</div>
<!-- Basic Auth -->
<div v-else-if="store.activeRequest.auth.type === 'basic'" class="flex flex-col gap-4 animate-fade-in">
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Username</label>
<div class="relative">
<User class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
type="text"
v-model="store.activeRequest.auth.basic.username"
placeholder="username"
class="w-full bg-slate-950 border border-slate-800 rounded-lg pl-10 pr-3 py-2 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none placeholder-slate-600"
/>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Password</label>
<div class="relative">
<Lock class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
:type="showPassword ? 'text' : 'password'"
v-model="store.activeRequest.auth.basic.password"
placeholder="password"
class="w-full bg-slate-950 border border-slate-800 rounded-lg pl-10 pr-3 py-2 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none placeholder-slate-600"
/>
<button
@click="showPassword = !showPassword"
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500 hover:text-indigo-400 font-medium"
>
{{ showPassword ? 'Hide' : 'Show' }}
</button>
</div>
</div>
</div>
<!-- Bearer Token -->
<div v-else-if="store.activeRequest.auth.type === 'bearer'" class="flex flex-col gap-4 animate-fade-in">
<div class="flex flex-col gap-1 h-full">
<label class="text-xs font-medium text-slate-400">Token</label>
<div class="relative flex-1 min-h-[120px]">
<div class="absolute top-3 left-3 text-xs font-mono text-slate-500 select-none">Bearer</div>
<textarea
v-model="store.activeRequest.auth.bearer.token"
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
class="w-full h-full bg-slate-950 border border-slate-800 rounded-lg pl-14 pr-3 py-2 text-sm font-mono text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none resize-none placeholder-slate-700 leading-relaxed"
></textarea>
</div>
</div>
</div>
<!-- API Key -->
<div v-else-if="store.activeRequest.auth.type === 'api_key'" class="flex flex-col gap-4 animate-fade-in">
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Key</label>
<div class="relative">
<Key class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
type="text"
v-model="store.activeRequest.auth.apiKey.key"
placeholder="x-api-key"
class="w-full bg-slate-950 border border-slate-800 rounded-lg pl-10 pr-3 py-2 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none placeholder-slate-600"
/>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Value</label>
<div class="relative">
<Fingerprint class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<input
:type="showApiKey ? 'text' : 'password'"
v-model="store.activeRequest.auth.apiKey.value"
placeholder="value"
class="w-full bg-slate-950 border border-slate-800 rounded-lg pl-10 pr-3 py-2 text-sm text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none placeholder-slate-600"
/>
<button
@click="showApiKey = !showApiKey"
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500 hover:text-indigo-400 font-medium"
>
{{ showApiKey ? 'Hide' : 'Show' }}
</button>
</div>
</div>
<div class="flex flex-col gap-1">
<label class="text-xs font-medium text-slate-400">Add To</label>
<div class="flex gap-4 mt-1">
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" v-model="store.activeRequest.auth.apiKey.addTo" value="header" class="accent-indigo-500" />
<span class="text-sm text-slate-300 group-hover:text-white">Header</span>
</label>
<label class="flex items-center gap-2 cursor-pointer group">
<input type="radio" v-model="store.activeRequest.auth.apiKey.addTo" value="query" class="accent-indigo-500" />
<span class="text-sm text-slate-300 group-hover:text-white">Query Params</span>
</label>
</div>
</div>
</div>
</div>
</div>
</template>
<style scoped>
.animate-fade-in {
animation: fadeIn 0.2s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(5px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@@ -3,6 +3,7 @@ import { ref } from 'vue';
import { useRequestStore } from '../stores/requestStore';
import { useSettingsStore } from '../stores/settingsStore';
import KeyValueEditor from './KeyValueEditor.vue';
import AuthPanel from './AuthPanel.vue';
import { invoke } from '@tauri-apps/api/core';
import { Codemirror } from 'vue-codemirror';
import { json } from '@codemirror/lang-json';
@@ -11,7 +12,7 @@ import { Play, Loader2 } from 'lucide-vue-next';
const store = useRequestStore();
const settings = useSettingsStore();
const activeTab = ref('params');
const activeTab = ref('auth'); // Default to auth or params? Spec says "add a new Tab... place it first". I'll set default to 'params' or 'auth'? Usually params is more common, but let's stick to 'params' as default unless user changed it, or maybe the spec implies it's important. I'll leave 'params' as default activeTab for now or switch to 'auth' if requested. Spec doesn't say to make it default active.
const isLoading = ref(false);
const extensions = [json(), oneDark];
@@ -38,6 +39,7 @@ const executeRequest = async () => {
headers,
body: store.activeRequest.body || null,
queryParams: params,
auth: store.activeRequest.auth,
});
// Update Store
@@ -95,7 +97,7 @@ const executeRequest = async () => {
<!-- Configuration Tabs -->
<div class="flex border-b border-slate-800">
<button
v-for="tab in ['params', 'headers', 'body']"
v-for="tab in ['auth', 'params', 'headers', 'body']"
:key="tab"
@click="activeTab = tab"
class="px-4 py-2 text-xs font-medium uppercase tracking-wide border-b-2 transition-colors"
@@ -107,6 +109,11 @@ const executeRequest = async () => {
<!-- Content Area -->
<div class="flex-1 overflow-hidden relative">
<KeepAlive>
<AuthPanel
v-if="activeTab === 'auth'"
/>
</KeepAlive>
<KeepAlive>
<KeyValueEditor
v-if="activeTab === 'params'"

View File

@@ -8,6 +8,13 @@ export interface KeyValue {
enabled: boolean;
}
export interface AuthState {
type: 'none' | 'basic' | 'bearer' | 'api_key';
basic: { username: '', password: '' };
bearer: { token: '' };
apiKey: { key: '', value: '', addTo: 'header' | 'query' };
}
export interface RequestData {
id: string;
method: string;
@@ -15,6 +22,7 @@ export interface RequestData {
params: KeyValue[];
headers: KeyValue[];
body: string;
auth: AuthState;
response?: {
status: number;
headers: Record<string, string>;
@@ -39,6 +47,12 @@ export const useRequestStore = defineStore('request', () => {
{ id: crypto.randomUUID(), key: '', value: '', enabled: true }
],
body: '',
auth: {
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' }
},
timestamp: Date.now(),
});
@@ -81,6 +95,16 @@ export const useRequestStore = defineStore('request', () => {
if (loaded.params.length === 0) loaded.params.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true });
if (loaded.headers.length === 0) loaded.headers.push({ id: crypto.randomUUID(), key: '', value: '', enabled: true });
// Ensure auth object exists (migration for old history)
if (!loaded.auth) {
loaded.auth = {
type: 'none',
basic: { username: '', password: '' },
bearer: { token: '' },
apiKey: { key: '', value: '', addTo: 'header' }
};
}
activeRequest.value = loaded;
};