add auth
This commit is contained in:
76
spec/feature-auth.md
Normal file
76
spec/feature-auth.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Feature Request: Authentication Support
|
||||
|
||||
**Context:**
|
||||
We are upgrading the "LiteRequest" Tauri + Vue application. Currently, it supports Headers, Params, and Body.
|
||||
**Goal:** Add a dedicated **"Auth"** tab in the Request Configuration area to support common authentication methods.
|
||||
|
||||
## 1. Functional Requirements
|
||||
|
||||
### 1.1 Supported Auth Types
|
||||
The user should be able to select one of the following from a dropdown:
|
||||
1. **No Auth** (Default)
|
||||
2. **Basic Auth**: Inputs for `Username` and `Password`.
|
||||
3. **Bearer Token**: Input for `Token` (e.g., JWT).
|
||||
4. **API Key**: Inputs for `Key`, `Value`, and a selection for `Add To` ("Header" or "Query Params").
|
||||
|
||||
### 1.2 State Management
|
||||
* Update `stores/requestStore.ts` to include an `auth` state object.
|
||||
* **Structure:**
|
||||
```typescript
|
||||
state: {
|
||||
// ... existing state
|
||||
auth: {
|
||||
type: 'none', // 'none' | 'basic' | 'bearer' | 'api_key'
|
||||
basic: { username: '', password: '' },
|
||||
bearer: { token: '' },
|
||||
apiKey: { key: '', value: '', addTo: 'header' } // addTo: 'header' | 'query'
|
||||
}
|
||||
}
|
||||
```
|
||||
* **Persistence:** These values should be saved to `localStorage` along with the request history (careful note: in a real app we would encrypt this, but for this local MVP, plain text storage is acceptable).
|
||||
|
||||
## 2. UI/UX Design
|
||||
|
||||
### 2.1 Request Panel Update
|
||||
* In `RequestPanel.vue`, add a new Tab labeled **"Auth"** (place it first, before "Params").
|
||||
* **Layout:**
|
||||
* **Type Selector:** A styled `<select>` or dropdown menu at the top of the tab content.
|
||||
* **Dynamic Form:**
|
||||
* If **No Auth**: Show a subtle message "This request does not use any authentication."
|
||||
* If **Basic Auth**: Two fields (Username, Password). *Password field must use type="password" with a toggle to show/hide.*
|
||||
* If **Bearer Token**: One large text area or input for the Token. Prefix it visually with "Bearer ".
|
||||
* If **API Key**: Three inputs: Key (text), Value (text/password), Add To (Radio button or Select).
|
||||
|
||||
### 2.2 Aesthetic Guidelines
|
||||
* Maintain the **Dark Mode** (Slate/Zinc) theme.
|
||||
* Inputs should look identical to the existing inputs in the "Headers" tab (rounded, dark background, border-slate-700).
|
||||
* Use `lucide-vue-next` icons (e.g., `ShieldCheck` icon for the Tab label).
|
||||
|
||||
## 3. Technical Implementation Details
|
||||
|
||||
### A. Frontend Logic (`requestStore.ts` & Component)
|
||||
* The Frontend needs to package this Auth data and send it to the Rust backend.
|
||||
* **Important:** Do *not* manually inject these into the `headers` list in the frontend UI (to avoid state sync issues). Pass the auth configuration as a separate object to the Rust command.
|
||||
|
||||
### B. Backend Logic (`main.rs`)
|
||||
* Update the `execute_request` Tauri command signature to accept an `auth` argument.
|
||||
* **Logic Update:**
|
||||
* Use `reqwest` built-in methods:
|
||||
* **Basic:** `.basic_auth(username, Some(password))`
|
||||
* **Bearer:** `.bearer_auth(token)`
|
||||
* **API Key:**
|
||||
* If `addTo == "header"`: Manually `.header(key, value)`
|
||||
* If `addTo == "query"`: Manually append to the query params.
|
||||
|
||||
## 4. Implementation Steps for AI
|
||||
|
||||
Please generate the code updates in the following order:
|
||||
|
||||
1. **Store Update:** Modify `stores/requestStore.ts` to add the `auth` state structure.
|
||||
2. **UI Component:** Create a new component `components/AuthPanel.vue` (or code to be added inside `RequestPanel.vue`) that handles the form switching and inputs.
|
||||
3. **Rust Backend:** Update `main.rs`. Define the `Auth` struct (using `serde::Deserialize`) and update the `execute_request` logic to apply the auth.
|
||||
4. **Integration:** Show how to update the `execute_request` invocation in the frontend to pass this new data.
|
||||
|
||||
---
|
||||
**Instruction to AI:**
|
||||
Generate the code to implement these Authentication features. Ensure the UI matches the existing modern style (Tailwind CSS).
|
||||
@@ -10,6 +10,35 @@ struct HttpResponse {
|
||||
time_elapsed: u128, // milliseconds
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct BasicAuth {
|
||||
username: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct BearerAuth {
|
||||
token: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct ApiKeyAuth {
|
||||
key: String,
|
||||
value: String,
|
||||
#[serde(rename = "addTo")]
|
||||
add_to: String, // "header" or "query"
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct AuthConfig {
|
||||
#[serde(rename = "type")]
|
||||
auth_type: String, // "none", "basic", "bearer", "api_key"
|
||||
basic: BasicAuth,
|
||||
bearer: BearerAuth,
|
||||
#[serde(rename = "apiKey")]
|
||||
api_key: ApiKeyAuth,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn execute_request(
|
||||
method: String,
|
||||
@@ -17,6 +46,7 @@ async fn execute_request(
|
||||
headers: HashMap<String, String>,
|
||||
body: Option<String>,
|
||||
query_params: Option<HashMap<String, String>>,
|
||||
auth: Option<AuthConfig>,
|
||||
) -> Result<HttpResponse, String> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(30))
|
||||
@@ -37,6 +67,31 @@ async fn execute_request(
|
||||
request_builder = request_builder.header(key, value);
|
||||
}
|
||||
|
||||
// Add Auth
|
||||
if let Some(auth_config) = auth {
|
||||
match auth_config.auth_type.as_str() {
|
||||
"basic" => {
|
||||
request_builder = request_builder.basic_auth(
|
||||
auth_config.basic.username,
|
||||
Some(auth_config.basic.password),
|
||||
);
|
||||
}
|
||||
"bearer" => {
|
||||
request_builder = request_builder.bearer_auth(auth_config.bearer.token);
|
||||
}
|
||||
"api_key" => {
|
||||
let key = auth_config.api_key.key;
|
||||
let value = auth_config.api_key.value;
|
||||
if auth_config.api_key.add_to == "header" {
|
||||
request_builder = request_builder.header(key, value);
|
||||
} else if auth_config.api_key.add_to == "query" {
|
||||
request_builder = request_builder.query(&[(key, value)]);
|
||||
}
|
||||
}
|
||||
_ => {} // "none" or unknown
|
||||
}
|
||||
}
|
||||
|
||||
// Add Body
|
||||
if let Some(b) = body {
|
||||
if !b.is_empty() {
|
||||
|
||||
153
src/components/AuthPanel.vue
Normal file
153
src/components/AuthPanel.vue
Normal 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>
|
||||
@@ -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'"
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user