Просмотр исходного кода

add auth UI and add it to request sending

biblius 1 неделя назад
Родитель
Сommit
1eb3016b1c

+ 1 - 0
src-tauri/migrations/20250922150745_init.up.sql

@@ -6,6 +6,7 @@ CREATE TABLE workspaces (
 CREATE TABLE auth(
 CREATE TABLE auth(
     id INTEGER PRIMARY KEY NOT NULL,
     id INTEGER PRIMARY KEY NOT NULL,
     workspace_id INTEGER NOT NULL,
     workspace_id INTEGER NOT NULL,
+    name TEXT NOT NULL,
     params JSONB NOT NULL,
     params JSONB NOT NULL,
     FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE
     FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE
 );
 );

+ 15 - 16
src-tauri/src/auth.rs

@@ -1,17 +1,16 @@
-use serde::{Deserialize, Serialize};
-use sqlx::SqlitePool;
-
 use crate::{
 use crate::{
     db,
     db,
     var::{expand_vars, parse_vars},
     var::{expand_vars, parse_vars},
     AppResult,
     AppResult,
 };
 };
+use serde::{Deserialize, Serialize};
+use sqlx::SqlitePool;
 
 
 #[derive(Debug, Deserialize)]
 #[derive(Debug, Deserialize)]
 pub enum AuthType {
 pub enum AuthType {
     Token,
     Token,
     Basic,
     Basic,
-    OAuth2,
+    OAuth,
 }
 }
 
 
 impl From<AuthType> for Auth {
 impl From<AuthType> for Auth {
@@ -19,7 +18,7 @@ impl From<AuthType> for Auth {
         match value {
         match value {
             AuthType::Token => Self::Token(TokenAuth::default()),
             AuthType::Token => Self::Token(TokenAuth::default()),
             AuthType::Basic => Self::Basic(BasicAuth::default()),
             AuthType::Basic => Self::Basic(BasicAuth::default()),
-            AuthType::OAuth2 => Self::OAuth2(OAuth::default()),
+            AuthType::OAuth => Self::OAuth(OAuth::default()),
         }
         }
     }
     }
 }
 }
@@ -27,15 +26,17 @@ impl From<AuthType> for Auth {
 #[derive(Debug, Serialize)]
 #[derive(Debug, Serialize)]
 pub struct Authentication {
 pub struct Authentication {
     pub id: i64,
     pub id: i64,
+    pub name: String,
     pub workspace_id: i64,
     pub workspace_id: i64,
     pub params: Auth,
     pub params: Auth,
 }
 }
 
 
 #[derive(Debug, Serialize, Deserialize)]
 #[derive(Debug, Serialize, Deserialize)]
+#[serde(tag = "type", content = "value")]
 pub enum Auth {
 pub enum Auth {
     Token(TokenAuth),
     Token(TokenAuth),
     Basic(BasicAuth),
     Basic(BasicAuth),
-    OAuth2(OAuth),
+    OAuth(OAuth),
 }
 }
 
 
 #[derive(Debug, Serialize, Deserialize, Default)]
 #[derive(Debug, Serialize, Deserialize, Default)]
@@ -43,23 +44,20 @@ pub struct TokenAuth {
     pub placement: TokenPlacement,
     pub placement: TokenPlacement,
     pub key: String,
     pub key: String,
 
 
+    pub name: String,
+
     /// The value will get expanded, therefore any variables present will expand to whatever is
     /// The value will get expanded, therefore any variables present will expand to whatever is
     /// in the env.
     /// in the env.
     pub value: String,
     pub value: String,
 }
 }
 
 
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Debug, Serialize, Deserialize, Default)]
 pub enum TokenPlacement {
 pub enum TokenPlacement {
     /// Holds the value for the key
     /// Holds the value for the key
-    Query(String),
+    Query,
     /// Holds the name for the header
     /// Holds the name for the header
-    Header(String),
-}
-
-impl Default for TokenPlacement {
-    fn default() -> Self {
-        Self::Header(String::default())
-    }
+    #[default]
+    Header,
 }
 }
 
 
 #[derive(Debug, Serialize, Deserialize, Default)]
 #[derive(Debug, Serialize, Deserialize, Default)]
@@ -86,6 +84,7 @@ pub struct OAuth {
 }
 }
 
 
 #[derive(Debug, Serialize, Deserialize, Default)]
 #[derive(Debug, Serialize, Deserialize, Default)]
+#[serde(tag = "type", content = "value")]
 pub enum GrantType {
 pub enum GrantType {
     #[default]
     #[default]
     AuthorizationCode,
     AuthorizationCode,
@@ -123,7 +122,7 @@ pub async fn expand_auth_vars(auth: &mut Auth, pool: &SqlitePool, env_id: i64) -
 
 
             *password = expand_vars(&password, &vars);
             *password = expand_vars(&password, &vars);
         }
         }
-        crate::auth::Auth::OAuth2(OAuth {
+        crate::auth::Auth::OAuth(OAuth {
             token_name,
             token_name,
             callback_url,
             callback_url,
             auth_url,
             auth_url,

+ 27 - 1
src-tauri/src/cmd.rs

@@ -473,8 +473,10 @@ pub async fn set_workspace_entry_auth(
     state: tauri::State<'_, AppState>,
     state: tauri::State<'_, AppState>,
     entry_id: i64,
     entry_id: i64,
     auth_id: Option<i64>,
     auth_id: Option<i64>,
+    inherit: Option<bool>,
 ) -> Result<(), String> {
 ) -> Result<(), String> {
-    if let Err(e) = db::set_workspace_entry_auth(state.db.clone(), entry_id, auth_id).await {
+    if let Err(e) = db::set_workspace_entry_auth(state.db.clone(), entry_id, auth_id, inherit).await
+    {
         return Err(e.to_string());
         return Err(e.to_string());
     }
     }
     Ok(())
     Ok(())
@@ -510,3 +512,27 @@ pub async fn delete_auth(state: tauri::State<'_, AppState>, id: i64) -> Result<(
         Err(e) => Err(e.to_string()),
         Err(e) => Err(e.to_string()),
     }
     }
 }
 }
+
+#[tauri::command]
+pub async fn update_auth(
+    state: tauri::State<'_, AppState>,
+    id: i64,
+    params: Auth,
+) -> Result<(), String> {
+    match db::update_auth(state.db.clone(), id, params).await {
+        Ok(_) => Ok(()),
+        Err(e) => Err(e.to_string()),
+    }
+}
+
+#[tauri::command]
+pub async fn rename_auth(
+    state: tauri::State<'_, AppState>,
+    id: i64,
+    name: String,
+) -> Result<(), String> {
+    match db::rename_auth(state.db.clone(), id, name).await {
+        Ok(_) => Ok(()),
+        Err(e) => Err(e.to_string()),
+    }
+}

+ 27 - 4
src-tauri/src/db.rs

@@ -764,7 +764,7 @@ pub async fn insert_auth(
     let json = Json(&params);
     let json = Json(&params);
 
 
     let record = sqlx::query!(
     let record = sqlx::query!(
-        "INSERT INTO auth(workspace_id, params) VALUES (?, ?) RETURNING id",
+        "INSERT INTO auth(workspace_id, name, params) VALUES (?, 'New authentication', ?) RETURNING id, name",
         workspace_id,
         workspace_id,
         json
         json
     )
     )
@@ -774,6 +774,7 @@ pub async fn insert_auth(
     Ok(Authentication {
     Ok(Authentication {
         id: record.id,
         id: record.id,
         workspace_id,
         workspace_id,
+        name: record.name,
         params,
         params,
     })
     })
 }
 }
@@ -788,7 +789,7 @@ pub async fn delete_auth(db: SqlitePool, id: i64) -> AppResult<()> {
 pub async fn list_auth(db: SqlitePool, workspace_id: i64) -> AppResult<Vec<Authentication>> {
 pub async fn list_auth(db: SqlitePool, workspace_id: i64) -> AppResult<Vec<Authentication>> {
     let records = sqlx::query!(
     let records = sqlx::query!(
         r#"
         r#"
-        SELECT id, workspace_id, params as "params: Json<Auth>"
+        SELECT id, name, workspace_id, params as "params: Json<Auth>"
         FROM auth
         FROM auth
         WHERE workspace_id = ?
         WHERE workspace_id = ?
         "#,
         "#,
@@ -801,6 +802,7 @@ pub async fn list_auth(db: SqlitePool, workspace_id: i64) -> AppResult<Vec<Authe
         .into_iter()
         .into_iter()
         .map(|record| Authentication {
         .map(|record| Authentication {
             id: record.id,
             id: record.id,
+            name: record.name,
             workspace_id: record.workspace_id,
             workspace_id: record.workspace_id,
             params: record.params.0,
             params: record.params.0,
         })
         })
@@ -810,7 +812,7 @@ pub async fn list_auth(db: SqlitePool, workspace_id: i64) -> AppResult<Vec<Authe
 pub async fn get_auth(db: SqlitePool, id: i64) -> AppResult<Authentication> {
 pub async fn get_auth(db: SqlitePool, id: i64) -> AppResult<Authentication> {
     let record = sqlx::query!(
     let record = sqlx::query!(
         r#"
         r#"
-        SELECT id, workspace_id, params as "params: Json<Auth>"
+        SELECT id, workspace_id, name, params as "params: Json<Auth>"
         FROM auth
         FROM auth
         WHERE id = ?
         WHERE id = ?
         "#,
         "#,
@@ -821,6 +823,7 @@ pub async fn get_auth(db: SqlitePool, id: i64) -> AppResult<Authentication> {
 
 
     Ok(Authentication {
     Ok(Authentication {
         id: record.id,
         id: record.id,
+        name: record.name,
         workspace_id: record.workspace_id,
         workspace_id: record.workspace_id,
         params: record.params.0,
         params: record.params.0,
     })
     })
@@ -830,10 +833,12 @@ pub async fn set_workspace_entry_auth(
     db: SqlitePool,
     db: SqlitePool,
     entry_id: i64,
     entry_id: i64,
     auth_id: Option<i64>,
     auth_id: Option<i64>,
+    inherit: Option<bool>,
 ) -> AppResult<()> {
 ) -> AppResult<()> {
     sqlx::query!(
     sqlx::query!(
-        "UPDATE workspace_entries SET auth = ? WHERE id = ?",
+        "UPDATE workspace_entries SET auth = ?, auth_inherit = COALESCE(?, auth_inherit) WHERE id = ?",
         auth_id,
         auth_id,
+        inherit,
         entry_id
         entry_id
     )
     )
     .execute(&db)
     .execute(&db)
@@ -842,6 +847,24 @@ pub async fn set_workspace_entry_auth(
     Ok(())
     Ok(())
 }
 }
 
 
+pub async fn update_auth(db: SqlitePool, auth_id: i64, params: Auth) -> AppResult<()> {
+    let params = Json(params);
+
+    sqlx::query!("UPDATE auth SET params = ? WHERE id = ?", params, auth_id)
+        .execute(&db)
+        .await?;
+
+    Ok(())
+}
+
+pub async fn rename_auth(db: SqlitePool, auth_id: i64, name: String) -> AppResult<()> {
+    sqlx::query!("UPDATE auth SET name = ? WHERE id = ?", name, auth_id)
+        .execute(&db)
+        .await?;
+
+    Ok(())
+}
+
 /// Check for the existence of an auth ID in the workspace entry. If one does not exist,
 /// Check for the existence of an auth ID in the workspace entry. If one does not exist,
 /// traverse its parents and attempt to find the first one that is present. If none exist,
 /// traverse its parents and attempt to find the first one that is present. If none exist,
 /// returns `None`.
 /// returns `None`.

+ 2 - 0
src-tauri/src/lib.rs

@@ -55,6 +55,8 @@ pub fn run() {
             cmd::insert_auth,
             cmd::insert_auth,
             cmd::set_workspace_entry_auth,
             cmd::set_workspace_entry_auth,
             cmd::list_auth,
             cmd::list_auth,
+            cmd::update_auth,
+            cmd::rename_auth,
             cmd::delete_auth,
             cmd::delete_auth,
         ])
         ])
         .run(tauri::generate_context!())
         .run(tauri::generate_context!())

+ 7 - 5
src-tauri/src/request.rs

@@ -25,6 +25,8 @@ pub const DEFAULT_HEADERS: &'static [(&'static str, &'static str)] = &[
 ];
 ];
 
 
 pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppResult<HttpResponse> {
 pub async fn send(client: reqwest::Client, req: HttpRequestParameters) -> AppResult<HttpResponse> {
+    dbg!(&req);
+
     let HttpRequestParameters {
     let HttpRequestParameters {
         url,
         url,
         method,
         method,
@@ -141,17 +143,17 @@ impl WorkspaceRequest {
     pub fn resolve_auth(&mut self, auth: Auth) -> AppResult<()> {
     pub fn resolve_auth(&mut self, auth: Auth) -> AppResult<()> {
         match auth {
         match auth {
             crate::auth::Auth::Token(token) => match token.placement {
             crate::auth::Auth::Token(token) => match token.placement {
-                crate::auth::TokenPlacement::Query(name) => {
+                crate::auth::TokenPlacement::Query => {
                     let mut url = RequestUrl::parse(&self.url)?;
                     let mut url = RequestUrl::parse(&self.url)?;
 
 
-                    url.query_params.push((&name, &token.value));
+                    url.query_params.push((&token.name, &token.value));
 
 
                     self.url = url.to_string();
                     self.url = url.to_string();
                 }
                 }
-                crate::auth::TokenPlacement::Header(name) => {
+                crate::auth::TokenPlacement::Header => {
                     self.headers.push(RequestHeader {
                     self.headers.push(RequestHeader {
                         id: -1,
                         id: -1,
-                        name,
+                        name: token.name,
                         value: token.value,
                         value: token.value,
                     });
                     });
                 }
                 }
@@ -167,7 +169,7 @@ impl WorkspaceRequest {
                     value,
                     value,
                 });
                 });
             }
             }
-            crate::auth::Auth::OAuth2(OAuth {
+            crate::auth::Auth::OAuth(OAuth {
                 token_name,
                 token_name,
                 callback_url,
                 callback_url,
                 auth_url,
                 auth_url,

+ 52 - 0
src/lib/components/Auth.svelte

@@ -0,0 +1,52 @@
+<script lang="ts">
+  import {
+    state as _state,
+    createAuth,
+    deleteAuth,
+    renameAuth,
+    updateAuthParams,
+  } from "$lib/state.svelte";
+
+  import { Button } from "$lib/components/ui/button";
+  import { Trash, Plus } from "@lucide/svelte";
+  import { Input } from "./ui/input";
+  import Editable from "./Editable.svelte";
+  import AuthParams from "./AuthParams.svelte";
+</script>
+
+<div class="space-y-6">
+  <!-- Inherit checkbox -->
+  <!-- <div class="flex items-center gap-3"> -->
+  <!-- <Checkbox checked={inherit} onCheckedChange={(v) => toggleInherit(!!v)} /> -->
+  <!-- <span class="text-sm">Inherit auth from parent collection</span> -->
+  <!-- </div> -->
+
+  <div class="border">
+    <h1 class="p-2">Workspace authentication</h1>
+    <p class="p-2 text-sm">
+      Create workspace-wide authentication schemes. Use the schemes created here
+      in request/collection tabs.
+    </p>
+  </div>
+
+  <!-- Create auth -->
+  <div class="space-y-2">
+    <div class="flex gap-2">
+      <Button variant="outline" onclick={() => createAuth("Token")}>
+        <Plus class="h-4 w-4 mr-2" /> Token
+      </Button>
+      <Button variant="outline" onclick={() => createAuth("Basic")}>
+        <Plus class="h-4 w-4 mr-2" /> Basic
+      </Button>
+      <Button variant="outline" onclick={() => createAuth("OAuth")}>
+        <Plus class="h-4 w-4 mr-2" /> OAuth
+      </Button>
+    </div>
+  </div>
+
+  {#each _state.auth as auth}
+    <div class="border p-2">
+      <AuthParams {auth} readonly={false} />
+    </div>
+  {/each}
+</div>

+ 49 - 0
src/lib/components/AuthParams.svelte

@@ -0,0 +1,49 @@
+<script lang="ts">
+  import {
+    state as _state,
+    createAuth,
+    deleteAuth,
+    renameAuth,
+    updateAuthParams,
+  } from "$lib/state.svelte";
+
+  let { auth, readonly }: { auth: Authentication; readonly: bool } = $props();
+
+  import { Button } from "$lib/components/ui/button";
+  import { Trash, Plus } from "@lucide/svelte";
+  import { Input } from "./ui/input";
+  import Editable from "./Editable.svelte";
+  import type { Authentication } from "$lib/types";
+</script>
+
+<div class="flex">
+  <Editable
+    bind:value={auth.name}
+    onSave={(value) => {
+      renameAuth(auth.id, value);
+    }}
+  >
+    {#snippet display({ value, startEdit })}
+      <h2 ondblclick={startEdit}>
+        {auth.name}
+      </h2>
+    {/snippet}
+  </Editable>
+  {#if !readonly}
+    <Trash onclick={() => deleteAuth(auth.id)} />
+  {/if}
+</div>
+{#if auth.params.type === "Token"}
+  <div>
+    <Input
+      bind:value={auth.params.value.name}
+      oninput={() => updateAuthParams(auth.id)}
+      {readonly}
+    ></Input>
+    <Input
+      bind:value={auth.params.value.value}
+      oninput={() => updateAuthParams(auth.id)}
+      {readonly}
+    ></Input>
+  </div>
+{/if}

+ 2 - 4
src/lib/components/Sidebar.svelte

@@ -3,8 +3,6 @@
   import * as DropdownMenu from "./ui/dropdown-menu/index";
   import * as DropdownMenu from "./ui/dropdown-menu/index";
   import { Button } from "$lib/components/ui/button/index.js";
   import { Button } from "$lib/components/ui/button/index.js";
   import type { Workspace } from "$lib/types";
   import type { Workspace } from "$lib/types";
-  import DropdownMenuLabel from "./ui/dropdown-menu/dropdown-menu-label.svelte";
-  import DropdownMenuSeparator from "./ui/dropdown-menu/dropdown-menu-separator.svelte";
   import { Input } from "./ui/input";
   import { Input } from "./ui/input";
   import { onMount } from "svelte";
   import { onMount } from "svelte";
   import {
   import {
@@ -62,7 +60,7 @@
           </DropdownMenu.Trigger>
           </DropdownMenu.Trigger>
 
 
           <DropdownMenu.Content align="start">
           <DropdownMenu.Content align="start">
-            <DropdownMenuLabel>New workspace</DropdownMenuLabel>
+            <DropdownMenu.Label>New workspace</DropdownMenu.Label>
             <form
             <form
               class="flex w-full max-w-sm items-center gap-2"
               class="flex w-full max-w-sm items-center gap-2"
               onsubmit={(e) => {
               onsubmit={(e) => {
@@ -81,7 +79,7 @@
               <Button type="submit" variant="outline">+</Button>
               <Button type="submit" variant="outline">+</Button>
             </form>
             </form>
 
 
-            <DropdownMenuSeparator />
+            <DropdownMenu.Separator />
             {#each workspaces as ws}
             {#each workspaces as ws}
               <DropdownMenu.Item
               <DropdownMenu.Item
                 disabled={ws.name === _state.workspace?.name}
                 disabled={ws.name === _state.workspace?.name}

+ 95 - 52
src/lib/components/WorkspaceEntry.svelte

@@ -1,4 +1,5 @@
 <script lang="ts">
 <script lang="ts">
+  import * as Select from "$lib/components/ui/select";
   import {
   import {
     state as _state,
     state as _state,
     deleteBody,
     deleteBody,
@@ -6,6 +7,7 @@
     insertHeader,
     insertHeader,
     selectEntry,
     selectEntry,
     sendRequest,
     sendRequest,
+    setEntryAuth,
     updateBodyContent,
     updateBodyContent,
     updateEntryName,
     updateEntryName,
     updateHeader,
     updateHeader,
@@ -23,6 +25,9 @@
   import { Loader, PlusIcon, TrashIcon } from "@lucide/svelte";
   import { Loader, PlusIcon, TrashIcon } from "@lucide/svelte";
   import CodeMirror from "./CodeMirror.svelte";
   import CodeMirror from "./CodeMirror.svelte";
   import * as Resizable from "$lib/components/ui/resizable/index";
   import * as Resizable from "$lib/components/ui/resizable/index";
+  import Auth from "./Auth.svelte";
+  import AuthParams from "./AuthParams.svelte";
+  import Checkbox from "./ui/checkbox/checkbox.svelte";
 
 
   let requestPane: Resizable.Pane;
   let requestPane: Resizable.Pane;
   let responsePane: Resizable.Pane;
   let responsePane: Resizable.Pane;
@@ -121,6 +126,14 @@
 
 
     return url;
     return url;
   }
   }
+
+  function resolveAuthName() {
+    if (_state.entry.auth !== null) {
+      return _state.auth.find((a) => a.id === _state.entry.auth).name;
+    } else {
+      return "None";
+    }
+  }
 </script>
 </script>
 
 
 <svelte:head>
 <svelte:head>
@@ -213,32 +226,27 @@
 
 
     <Resizable.PaneGroup direction="vertical" class="flex-1 w-full rounded-lg">
     <Resizable.PaneGroup direction="vertical" class="flex-1 w-full rounded-lg">
       <Resizable.Pane defaultSize={100} bind:this={requestPane}>
       <Resizable.Pane defaultSize={100} bind:this={requestPane}>
-        <Accordion.Root
-          type="multiple"
-          value={["auth", "params", "headers", "body"]}
-          class="h-full overflow-scroll"
-        >
-          <!-- URL PARAMS -->
-
-          {#if _state.entry.path.length > 0 || _state.entry.workingUrl?.query_params?.length > 0}
-            <Accordion.Item value="params">
-              <Accordion.Trigger class="transition-none!"
-                >Parameters</Accordion.Trigger
-              >
-
-              <!-- PATH PARAMS -->
-
-              <Accordion.Content
-                class="flex-col justify-center items-center space-y-4 "
-              >
-                <div class="flex flex-wrap">
-                  <h3 class="w-full mb-2 text-sm font-medium">Path</h3>
-                  <div class="w-1/2 grid grid-cols-2 gap-2 text-sm">
+        <Tabs.Root value="params" class="h-full flex flex-col">
+          <Tabs.List class="shrink-0">
+            <Tabs.Trigger value="params">Parameters</Tabs.Trigger>
+            <Tabs.Trigger value="headers">Headers</Tabs.Trigger>
+            <Tabs.Trigger value="body">Body</Tabs.Trigger>
+            <Tabs.Trigger value="auth">Auth</Tabs.Trigger>
+          </Tabs.List>
+
+          <div class="flex-1 overflow-auto p-2">
+            <!-- ================= PARAMETERS ================= -->
+
+            <Tabs.Content value="params" class="space-y-4">
+              {#if _state.entry.path.length > 0}
+                <div>
+                  <h3 class="mb-2 text-sm font-medium">Path</h3>
+                  <div class="grid grid-cols-2 gap-2 text-sm">
                     {#each _state.entry.path as param}
                     {#each _state.entry.path as param}
                       <Input
                       <Input
                         bind:value={param.name}
                         bind:value={param.name}
                         placeholder="key"
                         placeholder="key"
-                        oninput={(_) => handleUrlUpdate()}
+                        oninput={() => handleUrlUpdate()}
                       />
                       />
                       <Input
                       <Input
                         bind:value={param.value}
                         bind:value={param.value}
@@ -248,13 +256,13 @@
                     {/each}
                     {/each}
                   </div>
                   </div>
                 </div>
                 </div>
+              {/if}
 
 
-                <!-- QUERY PARAMS -->
-
-                {#if _state.entry.workingUrl?.query_params.length > 0}
-                  <h3 class="w-full mb-2 text-sm font-medium">Query</h3>
-                  <div class="grid items-center grid-cols-2 gap-2 text-sm">
-                    {#each _state.entry.workingUrl!!.query_params as param}
+              {#if _state.entry.workingUrl?.query_params.length > 0}
+                <div>
+                  <h3 class="mb-2 text-sm font-medium">Query</h3>
+                  <div class="grid grid-cols-2 gap-2 text-sm">
+                    {#each _state.entry.workingUrl.query_params as param}
                       <Input
                       <Input
                         bind:value={param[0]}
                         bind:value={param[0]}
                         placeholder="key"
                         placeholder="key"
@@ -267,21 +275,17 @@
                       />
                       />
                     {/each}
                     {/each}
                   </div>
                   </div>
-                {/if}
-              </Accordion.Content>
-            </Accordion.Item>
-          {/if}
+                </div>
+              {/if}
+            </Tabs.Content>
 
 
-          <!-- HEADERS -->
+            <!-- ================= HEADERS ================= -->
 
 
-          <Accordion.Item value="headers">
-            <Accordion.Trigger>Headers</Accordion.Trigger>
-            <Accordion.Content>
+            <Tabs.Content value="headers">
               <div class="grid grid-cols-3 gap-2 text-sm">
               <div class="grid grid-cols-3 gap-2 text-sm">
                 {#each _state.entry.headers as header}
                 {#each _state.entry.headers as header}
                   <div class="contents">
                   <div class="contents">
                     <Input
                     <Input
-                      class="w-full"
                       bind:value={header.name}
                       bind:value={header.name}
                       placeholder="Name"
                       placeholder="Name"
                       oninput={() =>
                       oninput={() =>
@@ -304,22 +308,20 @@
                     </div>
                     </div>
                   </div>
                   </div>
                 {/each}
                 {/each}
+
                 <PlusIcon
                 <PlusIcon
                   class="col-span-3 mt-2 cursor-pointer text-muted-foreground hover:text-primary"
                   class="col-span-3 mt-2 cursor-pointer text-muted-foreground hover:text-primary"
                   onclick={() => insertHeader()}
                   onclick={() => insertHeader()}
                 />
                 />
               </div>
               </div>
-            </Accordion.Content>
-          </Accordion.Item>
+            </Tabs.Content>
 
 
-          <!-- BODY -->
+            <!-- ================= BODY ================= -->
 
 
-          <Accordion.Item value="body">
-            <Accordion.Trigger>Body</Accordion.Trigger>
-            <Accordion.Content class="space-y-4">
+            <Tabs.Content value="body" class="space-y-4">
               <Tabs.Root value={_state.entry.body === null ? "none" : "json"}>
               <Tabs.Root value={_state.entry.body === null ? "none" : "json"}>
                 <Tabs.List>
                 <Tabs.List>
-                  <Tabs.Trigger value="none" onclick={() => deleteBody()}
+                  <Tabs.Trigger value="none" onclick={deleteBody}
                     >None</Tabs.Trigger
                     >None</Tabs.Trigger
                   >
                   >
                   <Tabs.Trigger value="json">JSON</Tabs.Trigger>
                   <Tabs.Trigger value="json">JSON</Tabs.Trigger>
@@ -327,17 +329,13 @@
                   <Tabs.Trigger value="text">Text</Tabs.Trigger>
                   <Tabs.Trigger value="text">Text</Tabs.Trigger>
                 </Tabs.List>
                 </Tabs.List>
 
 
-                <Tabs.Content value="none"></Tabs.Content>
-
                 <Tabs.Content value="json">
                 <Tabs.Content value="json">
                   <CodeMirror
                   <CodeMirror
                     input={_state.entry.body?.body}
                     input={_state.entry.body?.body}
                     onStateChange={(update) => {
                     onStateChange={(update) => {
-                      // console.log(update);
                       if (
                       if (
                         update.docChanged &&
                         update.docChanged &&
-                        _state.entry!!.body?.body !==
-                          update.state.doc.toString()
+                        _state.entry.body?.body !== update.state.doc.toString()
                       ) {
                       ) {
                         updateBodyContent(update.state.doc.toString(), "Json");
                         updateBodyContent(update.state.doc.toString(), "Json");
                       }
                       }
@@ -358,9 +356,54 @@
                   ></textarea>
                   ></textarea>
                 </Tabs.Content>
                 </Tabs.Content>
               </Tabs.Root>
               </Tabs.Root>
-            </Accordion.Content>
-          </Accordion.Item>
-        </Accordion.Root>
+            </Tabs.Content>
+
+            <Tabs.Content value="auth" class="space-y-4">
+              <div class="flex items-center">
+                <p class="mr-2">Inherit</p>
+                <Checkbox
+                  checked={_state.entry.auth_inherit}
+                  onCheckedChange={(v) => setEntryAuth(_state.entry.auth, v)}
+                />
+              </div>
+              <div
+                class="transition-opacity"
+                class:opacity-50={_state.entry.auth_inherit}
+                class:pointer-events-none={_state.entry.auth_inherit}
+              >
+                <Select.Root type="single" value={resolveAuthName()}>
+                  <Select.Trigger class="w-64">
+                    {resolveAuthName()}
+                  </Select.Trigger>
+
+                  <Select.Content>
+                    <Select.Item
+                      onclick={() => setEntryAuth(null, null)}
+                      value={"None"}
+                    >
+                      None
+                    </Select.Item>
+                    {#each _state.auth as auth}
+                      <Select.Item
+                        onclick={() => setEntryAuth(auth.id, null)}
+                        value={auth.name}
+                      >
+                        {auth.name}
+                      </Select.Item>
+                    {/each}
+                  </Select.Content>
+                </Select.Root>
+
+                {#if _state.entry.auth}
+                  <AuthParams
+                    auth={_state.auth.find((a) => a.id === _state.entry.auth)!!}
+                    readonly={true}
+                  />
+                {/if}
+              </div>
+            </Tabs.Content>
+          </div>
+        </Tabs.Root>
       </Resizable.Pane>
       </Resizable.Pane>
 
 
       <Resizable.Handle
       <Resizable.Handle

+ 36 - 0
src/lib/components/ui/checkbox/checkbox.svelte

@@ -0,0 +1,36 @@
+<script lang="ts">
+	import { Checkbox as CheckboxPrimitive } from "bits-ui";
+	import CheckIcon from "@lucide/svelte/icons/check";
+	import MinusIcon from "@lucide/svelte/icons/minus";
+	import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
+
+	let {
+		ref = $bindable(null),
+		checked = $bindable(false),
+		indeterminate = $bindable(false),
+		class: className,
+		...restProps
+	}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> = $props();
+</script>
+
+<CheckboxPrimitive.Root
+	bind:ref
+	data-slot="checkbox"
+	class={cn(
+		"border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive peer flex size-4 shrink-0 items-center justify-center rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
+		className
+	)}
+	bind:checked
+	bind:indeterminate
+	{...restProps}
+>
+	{#snippet children({ checked, indeterminate })}
+		<div data-slot="checkbox-indicator" class="text-current transition-none">
+			{#if checked}
+				<CheckIcon class="size-3.5" />
+			{:else if indeterminate}
+				<MinusIcon class="size-3.5" />
+			{/if}
+		</div>
+	{/snippet}
+</CheckboxPrimitive.Root>

+ 6 - 0
src/lib/components/ui/checkbox/index.ts

@@ -0,0 +1,6 @@
+import Root from "./checkbox.svelte";
+export {
+	Root,
+	//
+	Root as Checkbox,
+};

+ 81 - 5
src/lib/state.svelte.ts

@@ -9,6 +9,8 @@ import type {
   RequestUrl,
   RequestUrl,
   RequestHeader,
   RequestHeader,
   RequestPathParam,
   RequestPathParam,
+  Authentication,
+  AuthType,
 } from "./types";
 } from "./types";
 import { getSetting, setSetting } from "./settings.svelte";
 import { getSetting, setSetting } from "./settings.svelte";
 
 
@@ -47,6 +49,8 @@ export type WorkspaceState = {
    * Currently selected environment.
    * Currently selected environment.
    */
    */
   environment: WorkspaceEnvironment | null;
   environment: WorkspaceEnvironment | null;
+
+  auth: Authentication[];
 };
 };
 
 
 export const state: WorkspaceState = $state({
 export const state: WorkspaceState = $state({
@@ -57,6 +61,7 @@ export const state: WorkspaceState = $state({
   indexes: {},
   indexes: {},
   environments: [],
   environments: [],
   environment: null,
   environment: null,
+  auths: [],
 });
 });
 
 
 const index = (entry: WorkspaceEntry) => {
 const index = (entry: WorkspaceEntry) => {
@@ -83,7 +88,10 @@ function reset() {
   state.environments = [];
   state.environments = [];
 }
 }
 
 
-export async function selectEnvironment(id: number | null) {
+export async function selectEnvironment(
+  id: number | null,
+  save: boolean = true,
+) {
   if (id === null) {
   if (id === null) {
     state.environment = null;
     state.environment = null;
     let env = await getSetting("lastEnvironment");
     let env = await getSetting("lastEnvironment");
@@ -98,14 +106,20 @@ export async function selectEnvironment(id: number | null) {
 
 
   state.environment = state.environments.find((e) => e.id === id) ?? null;
   state.environment = state.environments.find((e) => e.id === id) ?? null;
 
 
+  console.debug("selected environment:", state.environment?.name);
+
+  if (!save) {
+    return;
+  }
+
   let env = await getSetting("lastEnvironment");
   let env = await getSetting("lastEnvironment");
   if (env) {
   if (env) {
     env[state.workspace!!.id] = id;
     env[state.workspace!!.id] = id;
   } else {
   } else {
     env = { [state.workspace!!.id]: id };
     env = { [state.workspace!!.id]: id };
   }
   }
+
   setSetting("lastEnvironment", env);
   setSetting("lastEnvironment", env);
-  console.debug("selected environment:", state.environment?.name);
 }
 }
 
 
 export function selectWorkspace(ws: Workspace) {
 export function selectWorkspace(ws: Workspace) {
@@ -188,6 +202,7 @@ export async function loadWorkspace(ws: Workspace) {
   }
   }
 
 
   await loadEnvironments(state.workspace.id);
   await loadEnvironments(state.workspace.id);
+  await loadAuths(state.workspace.id);
 }
 }
 
 
 export function createRequest(parentId?: number) {
 export function createRequest(parentId?: number) {
@@ -247,12 +262,16 @@ export async function loadEnvironments(workspaceId: number) {
     "list_environments",
     "list_environments",
     { workspaceId },
     { workspaceId },
   );
   );
-  const lastEnv = (await getSetting("lastEnvironment"))?.[workspaceId];
-  if (lastEnv) {
-    selectEnvironment(lastEnv);
+  const lastEnv = await getSetting("lastEnvironment");
+  if (lastEnv && lastEnv[workspaceId] !== undefined) {
+    selectEnvironment(lastEnv[workspaceId], false);
   }
   }
 }
 }
 
 
+export async function loadAuths(workspaceId: number) {
+  state.auth = await invoke<Authentication[]>("list_auth", { workspaceId });
+}
+
 export async function createEnvironment(workspaceId: number, name: string) {
 export async function createEnvironment(workspaceId: number, name: string) {
   console.debug("creating environment in", workspaceId);
   console.debug("creating environment in", workspaceId);
   const env = await invoke<WorkspaceEnvironment>("create_env", {
   const env = await invoke<WorkspaceEnvironment>("create_env", {
@@ -466,6 +485,63 @@ export async function updateBodyContent(body: string, ct: string) {
   console.debug("Updated body content to", $state.snapshot(state.entry!!.body));
   console.debug("Updated body content to", $state.snapshot(state.entry!!.body));
 }
 }
 
 
+export async function createAuth(type: AuthType) {
+  const auth = await invoke<Authentication>("insert_auth", {
+    workspaceId: state.workspace!!.id,
+    type,
+  });
+  console.debug("created auth", auth);
+  state.auth.unshift(auth);
+}
+
+export async function renameAuth(id: number, name: string) {
+  await invoke<Authentication>("rename_auth", {
+    id,
+    name,
+  });
+  const auth = state.auth.find((a) => a.id === id);
+  auth!!.name = name;
+}
+
+export async function updateAuthParams(id: number) {
+  const auth = state.auth.find((a) => a.id === id);
+
+  console.debug("updating auth params", $state.snapshot(auth));
+
+  if (!auth) {
+    console.warn("Attempted to update non-existing auth", id);
+    return;
+  }
+
+  await invoke<Authentication>("update_auth", {
+    id,
+    params: auth.params,
+  });
+}
+
+export async function deleteAuth(id: number) {
+  console.debug("deleting auth", id);
+
+  await invoke("delete_auth", { id });
+
+  state.auth = state.auth.filter((a) => a.id !== id);
+}
+
+export async function setEntryAuth(id: number | null, inherit: boolean | null) {
+  console.debug("setting entry auth to", id);
+
+  await invoke("set_workspace_entry_auth", {
+    entryId: state.entry!!.id,
+    authId: id,
+    inherit,
+  });
+
+  state.entry.auth = id;
+  if (inherit !== null) {
+    state.entry.auth_inherit = inherit;
+  }
+}
+
 type WorkspaceEntryResponse =
 type WorkspaceEntryResponse =
   | {
   | {
       type: "Collection";
       type: "Collection";

+ 43 - 0
src/lib/types.ts

@@ -97,3 +97,46 @@ export type EnvVariable = {
   value: string;
   value: string;
   secret: boolean;
   secret: boolean;
 };
 };
+
+export type Authentication = {
+  id: number;
+  workspace_id: number;
+  name: string;
+  params: AuthParams;
+};
+
+export type AuthParams =
+  | {
+      type: "Token";
+      value: {
+        name: string;
+        placement: "Header" | "Query";
+        value: string;
+      };
+    }
+  | { type: "Basic"; value: { username: string; password: string } }
+  | {
+      type: "OAuth";
+      value: {
+        token_name: string;
+        callback_url: string;
+        auth_url: string;
+        token_url: string;
+        refresh_url: string | null;
+        client_id: string;
+        client_secret: string;
+        scope: string | null;
+        state: string | null;
+        grant_type:
+          | {
+              type: "AuthorizationCode";
+            }
+          | {
+              type: "AuthorizationCodePKCE";
+              value: { verifier: string | null };
+            }
+          | { type: "ClientCredentials" };
+      };
+    };
+
+export type AuthType = "Token" | "Basic" | "OAuth";

+ 21 - 5
src/routes/+page.svelte

@@ -3,14 +3,15 @@
   import AppSidebar from "$lib/components/Sidebar.svelte";
   import AppSidebar from "$lib/components/Sidebar.svelte";
   import { toggleMode } from "mode-watcher";
   import { toggleMode } from "mode-watcher";
   import { Button } from "$lib/components/ui/button";
   import { Button } from "$lib/components/ui/button";
-  import { SunIcon, MoonIcon } from "@lucide/svelte";
+  import { SunIcon, MoonIcon, Lock } from "@lucide/svelte";
   import WorkspaceEntry from "$lib/components/WorkspaceEntry.svelte";
   import WorkspaceEntry from "$lib/components/WorkspaceEntry.svelte";
   import { state as _state, selectEnvironment } from "$lib/state.svelte";
   import { state as _state, selectEnvironment } from "$lib/state.svelte";
   import { SlidersHorizontal } from "@lucide/svelte";
   import { SlidersHorizontal } from "@lucide/svelte";
   import Environment from "$lib/components/Environment.svelte";
   import Environment from "$lib/components/Environment.svelte";
   import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index";
   import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index";
+  import Auth from "$lib/components/Auth.svelte";
 
 
-  let displayEnvs = $state(false);
+  let displayModal: "env" | "auth" | null = $state(null);
 </script>
 </script>
 
 
 <Sidebar.Provider>
 <Sidebar.Provider>
@@ -51,8 +52,10 @@
         </DropdownMenu.Content>
         </DropdownMenu.Content>
       </DropdownMenu.Root>
       </DropdownMenu.Root>
     </header>
     </header>
-    {#if displayEnvs}
+    {#if displayModal === "env"}
       <Environment />
       <Environment />
+    {:else if displayModal === "auth"}
+      <Auth />
     {:else if _state.entry}
     {:else if _state.entry}
       <WorkspaceEntry />
       <WorkspaceEntry />
     {:else}{/if}
     {:else}{/if}
@@ -75,14 +78,27 @@
 
 
         <Sidebar.MenuItem class="pt-2">
         <Sidebar.MenuItem class="pt-2">
           <Button
           <Button
-            onclick={() => (displayEnvs = !displayEnvs)}
+            onclick={() =>
+              (displayModal = displayModal === "env" ? null : "env")}
             variant="ghost"
             variant="ghost"
             size="icon-sm"
             size="icon-sm"
           >
           >
-            <SlidersHorizontal class="" />
+            <SlidersHorizontal />
             <span class="sr-only">Display environment</span>
             <span class="sr-only">Display environment</span>
           </Button>
           </Button>
         </Sidebar.MenuItem>
         </Sidebar.MenuItem>
+
+        <Sidebar.MenuItem class="pt-2">
+          <Button
+            onclick={() =>
+              (displayModal = displayModal === "auth" ? null : "auth")}
+            variant="ghost"
+            size="icon-sm"
+          >
+            <Lock />
+            <span class="sr-only">Display auth</span>
+          </Button>
+        </Sidebar.MenuItem>
       </Sidebar.Menu>
       </Sidebar.Menu>
     </Sidebar.Root>
     </Sidebar.Root>
   </Sidebar.Provider>
   </Sidebar.Provider>