feat(v3): migrate workspace to API, Tauri desktop, and v3 crates; remove legacy MCP stack
Some checks failed
Secrets v3 CI / 检查 (push) Has been cancelled
Some checks failed
Secrets v3 CI / 检查 (push) Has been cancelled
- Add apps/api, desktop Tauri shell, domain/application/crypto/device-auth/infrastructure-db - Replace desktop-daemon vault integration; drop secrets-core and secrets-mcp* - Ignore apps/desktop/dist and generated Tauri icons; document icon/dist steps in AGENTS.md - Apply rustfmt; fix clippy (collapsible_if, HTTP method as str)
This commit is contained in:
6
apps/desktop/README.md
Normal file
6
apps/desktop/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# apps/desktop
|
||||
|
||||
This directory is reserved for the v3 Tauri desktop shell.
|
||||
|
||||
The desktop UI is intentionally kept separate from `crates/desktop-daemon` so
|
||||
that closing the main window does not terminate the local MCP process.
|
||||
208
apps/desktop/design/DESIGN.md
Normal file
208
apps/desktop/design/DESIGN.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Secrets Design System
|
||||
|
||||
## 1. Visual Theme & Atmosphere
|
||||
|
||||
- Primary inspiration: Raycast desktop UI.
|
||||
- Secondary influence: Linear information density and list discipline.
|
||||
- Product personality: secure, local-first, developer-facing, restrained, trustworthy.
|
||||
- Default mood: dark utility app, not a marketing site and not a glossy consumer app.
|
||||
- The interface should feel like a native desktop control surface for secrets and MCP integrations.
|
||||
- Use calm contrast, clean edges, compact spacing, and intentional empty space.
|
||||
- Prefer precision over decoration. Visual polish should come from alignment, spacing, and hierarchy.
|
||||
|
||||
## 2. Color Palette & Roles
|
||||
|
||||
### Core Surfaces
|
||||
|
||||
- `bg.app`: `#0A0A0B` - app background, deepest canvas.
|
||||
- `bg.panel`: `#111113` - main panel and modal background.
|
||||
- `bg.panelElevated`: `#17171A` - cards, selected rows, input shells.
|
||||
- `bg.panelHover`: `#1D1D22` - hover state for rows and controls.
|
||||
- `bg.input`: `#141418` - text inputs, code blocks, secret fields.
|
||||
- `border.subtle`: `#26262C` - default panel borders.
|
||||
- `border.strong`: `#34343D` - active borders and high-emphasis outlines.
|
||||
|
||||
### Text
|
||||
|
||||
- `text.primary`: `#F5F5F7` - primary labels and values.
|
||||
- `text.secondary`: `#B3B3BD` - supporting metadata.
|
||||
- `text.tertiary`: `#7C7C88` - placeholders and low-emphasis copy.
|
||||
- `text.inverse`: `#0B0B0D` - text on bright accents.
|
||||
|
||||
### Accents
|
||||
|
||||
- `accent.blue`: `#3B82F6` - login CTA, toggles, focus ring, trust signals.
|
||||
- `accent.blueHover`: `#4C8DFF` - hover state for primary interactions.
|
||||
- `accent.purple`: `#8B5CF6` - secondary accent for selected count pills or light emphasis.
|
||||
- `accent.amber`: `#D97706` - local warnings or pending states.
|
||||
- `accent.red`: `#EF4444` - destructive actions.
|
||||
- `accent.green`: `#22C55E` - success or enabled state when stronger signal is required.
|
||||
|
||||
### Semantic Use
|
||||
|
||||
- Blue is the main action color. Keep it rare and meaningful.
|
||||
- Purple can appear in subtle badges or selected-count chips, never as a second primary CTA.
|
||||
- Red is reserved for delete, revoke, sign-out danger, and destructive confirmations.
|
||||
- Avoid bright gradients as a dominant surface treatment.
|
||||
|
||||
## 3. Typography Rules
|
||||
|
||||
- Font stack: `Inter`, `SF Pro Text`, `SF Pro Display`, `Segoe UI`, system sans-serif.
|
||||
- Use system-friendly text rendering. This is a desktop tool, not a display-heavy website.
|
||||
- Chinese UI copy is allowed and should feel natural beside English identifiers like `host`, `token`, `MCP`.
|
||||
- Keep tracking neutral. Avoid wide uppercase spacing except tiny overline labels.
|
||||
|
||||
### Type Scale
|
||||
|
||||
- App title / page title: 30-34px, weight 700.
|
||||
- Section title: 18-22px, weight 650-700.
|
||||
- Card title / row title: 15-17px, weight 600.
|
||||
- Body text: 13-14px, weight 400-500.
|
||||
- Caption / metadata label: 11-12px, weight 500, uppercase allowed with modest tracking.
|
||||
- Monospace values: `SF Mono`, `JetBrains Mono`, `Menlo`, monospace; 12-13px.
|
||||
|
||||
## 4. Component Stylings
|
||||
|
||||
### App Shell
|
||||
|
||||
- Use a three-pane desktop layout for the main screen: left navigation, middle list, right detail pane.
|
||||
- Pane separation should rely on subtle borders, not strong shadows.
|
||||
- Sidebar should feel slightly darker than the center list pane.
|
||||
- The detail pane can be the most open surface, with larger top padding and calmer spacing.
|
||||
|
||||
### Login Card
|
||||
|
||||
- Centered card on a dark canvas.
|
||||
- Width: compact, roughly 420-520px.
|
||||
- Rounded corners: 24-28px.
|
||||
- Include one lock/trust mark, one clear product title, one short support sentence, one primary Google login button.
|
||||
- Login should feel calm and premium, never busy.
|
||||
|
||||
### Buttons
|
||||
|
||||
- Primary button: dark app shell with blue fill, white text, medium radius.
|
||||
- Secondary button: dark raised surface with subtle border.
|
||||
- Destructive button: same structure as secondary, with red text or red-emphasis border only when needed.
|
||||
- Button height should feel desktop-like, not mobile oversized.
|
||||
- Avoid flashy gradients and oversized glows.
|
||||
|
||||
### Inputs
|
||||
|
||||
- Inputs use dark filled surfaces, subtle inset feel, 12-14px radius.
|
||||
- Border should be nearly invisible at rest and stronger on hover/focus.
|
||||
- Placeholders should be quiet and low-contrast.
|
||||
- Search and filter inputs should visually align and share the same height.
|
||||
|
||||
### Lists and Rows
|
||||
|
||||
- Entry rows should be compact, crisp, and easy to scan.
|
||||
- Selected row: slightly brighter dark card, subtle border, no heavy glow.
|
||||
- Support a two-line rhythm: primary name and smaller type/folder metadata.
|
||||
- Counts in the sidebar should use muted rounded chips.
|
||||
|
||||
### Detail Pane
|
||||
|
||||
- Use strong top title hierarchy with restrained action buttons on the right.
|
||||
- Metadata should be presented in structured blocks or columns, not loose paragraphs.
|
||||
- Secret values should live inside dedicated protected field cards.
|
||||
- Secret field rows should include icon, masked value, reveal action, and copy action.
|
||||
- Sensitive content must look controlled and deliberate, not playful.
|
||||
|
||||
### Modals
|
||||
|
||||
- Modal cards should feel like elevated control panels.
|
||||
- MCP integration modal should support stacked integration rows with trailing toggles.
|
||||
- Embedded JSON/config blocks should use a darker, code-oriented surface with monospace text.
|
||||
- Large modal width is acceptable for configuration-heavy content.
|
||||
|
||||
### Toggles
|
||||
|
||||
- Use blue enabled state by default.
|
||||
- Toggle track should be compact and clean, avoiding iOS-like softness.
|
||||
- Align toggles flush right in integration lists.
|
||||
|
||||
### Badges and Status Pills
|
||||
|
||||
- Use small rounded pills for folder counts, archived state, or recent-delete state.
|
||||
- Prefer muted purple, gray, or amber fills over saturated color blocks.
|
||||
|
||||
## 5. Layout Principles
|
||||
|
||||
- Use an 8px spacing system.
|
||||
- Typical paddings:
|
||||
- Sidebars: 16-20px.
|
||||
- List and toolbar: 12-18px.
|
||||
- Detail pane: 24-32px.
|
||||
- Modals: 20-28px.
|
||||
- Favor even vertical rhythm over decorative separators.
|
||||
- Keep left edges aligned aggressively across sections.
|
||||
- Avoid oversized hero spacing inside application surfaces.
|
||||
- The main app should feel dense enough for productivity but never cramped.
|
||||
|
||||
## 6. Depth & Elevation
|
||||
|
||||
- Most separation should come from tone shifts and borders.
|
||||
- Base panels: no shadow or extremely soft shadow.
|
||||
- Elevated cards and modals: subtle shadow only, with low blur and low opacity.
|
||||
- Do not use neon bloom, oversized backdrop blur, or glassmorphism.
|
||||
- Focus states should use border color and a faint blue outer ring.
|
||||
|
||||
## 7. Do's and Don'ts
|
||||
|
||||
### Do
|
||||
|
||||
- Keep the UI dark, crisp, and desktop-native.
|
||||
- Preserve strong information hierarchy in the detail pane.
|
||||
- Make security-sensitive actions feel explicit and carefully gated.
|
||||
- Use compact controls and disciplined spacing.
|
||||
- Let alignment and typography carry most of the visual quality.
|
||||
- Keep MCP integration screens structured like settings panels.
|
||||
|
||||
### Don't
|
||||
|
||||
- Do not turn the app into a landing page aesthetic.
|
||||
- Do not use giant gradients, colorful illustrations, or soft SaaS cards.
|
||||
- Do not over-round every surface.
|
||||
- Do not mix many accent colors in one screen.
|
||||
- Do not make secret fields look like casual form inputs.
|
||||
- Do not use bright white backgrounds in the desktop app.
|
||||
|
||||
## 8. Responsive Behavior
|
||||
|
||||
- Primary target is desktop widths from 1280px upward.
|
||||
- The three-pane shell should remain stable on desktop.
|
||||
- At narrower widths, collapse from three panes to two panes before using stacked mobile behavior.
|
||||
- The MCP modal can reduce width but should keep readable row spacing and code block legibility.
|
||||
- Buttons and toggles should remain mouse-first, with minimum 32px touch-friendly height where practical.
|
||||
|
||||
## 9. Screen-Specific Guidance
|
||||
|
||||
### Login Screen
|
||||
|
||||
- Centered trust card.
|
||||
- One focal icon or emblem above the title.
|
||||
- Keep copy short.
|
||||
- The Google login button should be the visual anchor.
|
||||
|
||||
### Main Secrets Screen
|
||||
|
||||
- Left sidebar: user card, folder navigation, utility actions near the bottom.
|
||||
- Middle pane: search, type filter, result list.
|
||||
- Right pane: selected entry title, metadata grid, secret cards, edit actions.
|
||||
- The selected item should be immediately obvious but understated.
|
||||
|
||||
### MCP Integration Screen
|
||||
|
||||
- Treat as a settings modal.
|
||||
- Integration rows should read like desktop preferences, not marketing feature cards.
|
||||
- JSON config block should feel developer-native and copy-friendly.
|
||||
|
||||
## 10. Agent Prompt Guide
|
||||
|
||||
- Keywords: `dark desktop utility`, `Raycast-inspired`, `Linear-density`, `secure control panel`, `developer tool`, `restrained premium`, `MCP settings modal`.
|
||||
- When generating screens, preserve: dark surfaces, subtle borders, compact controls, right-aligned actions, clean typography, muted status pills.
|
||||
- If unsure, bias toward less decoration and tighter structure.
|
||||
|
||||
## 11. Quick Summary for Agents
|
||||
|
||||
Build Secrets like a polished desktop utility: mostly Raycast in atmosphere, a little Linear in density, with dark layered panels, precise typography, subtle borders, blue-only primary actions, and security-sensitive detail cards that feel calm, serious, and highly usable.
|
||||
6300
apps/desktop/design/secrets-client.pen
Normal file
6300
apps/desktop/design/secrets-client.pen
Normal file
File diff suppressed because it is too large
Load Diff
32
apps/desktop/src-tauri/Cargo.toml
Normal file
32
apps/desktop/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "secrets-desktop"
|
||||
version = "3.0.0"
|
||||
edition.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
axum.workspace = true
|
||||
chrono.workspace = true
|
||||
hex.workspace = true
|
||||
sqlx.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tauri.workspace = true
|
||||
tokio.workspace = true
|
||||
reqwest.workspace = true
|
||||
sha2.workspace = true
|
||||
url.workspace = true
|
||||
uuid.workspace = true
|
||||
base64 = "0.22.1"
|
||||
|
||||
secrets-client-integrations = { path = "../../../crates/client-integrations" }
|
||||
secrets-crypto = { path = "../../../crates/crypto" }
|
||||
secrets-device-auth = { path = "../../../crates/device-auth" }
|
||||
secrets-domain = { path = "../../../crates/domain" }
|
||||
|
||||
[[bin]]
|
||||
name = "Secrets"
|
||||
path = "src/main.rs"
|
||||
3
apps/desktop/src-tauri/build.rs
Normal file
3
apps/desktop/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
2
apps/desktop/src-tauri/check_png_center.js
Normal file
2
apps/desktop/src-tauri/check_png_center.js
Normal file
@@ -0,0 +1,2 @@
|
||||
const fs = require('fs');
|
||||
// Very simple check: read the first few bytes, maybe we can use an image library to find the bounding box
|
||||
1
apps/desktop/src-tauri/gen/schemas/acl-manifests.json
Normal file
1
apps/desktop/src-tauri/gen/schemas/acl-manifests.json
Normal file
File diff suppressed because one or more lines are too long
1
apps/desktop/src-tauri/gen/schemas/capabilities.json
Normal file
1
apps/desktop/src-tauri/gen/schemas/capabilities.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
2244
apps/desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
2244
apps/desktop/src-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
2244
apps/desktop/src-tauri/gen/schemas/macOS-schema.json
Normal file
2244
apps/desktop/src-tauri/gen/schemas/macOS-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
apps/desktop/src-tauri/icons/icon.png
Normal file
BIN
apps/desktop/src-tauri/icons/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
1427
apps/desktop/src-tauri/src/local_vault.rs
Normal file
1427
apps/desktop/src-tauri/src/local_vault.rs
Normal file
File diff suppressed because it is too large
Load Diff
1179
apps/desktop/src-tauri/src/main.rs
Normal file
1179
apps/desktop/src-tauri/src/main.rs
Normal file
File diff suppressed because it is too large
Load Diff
356
apps/desktop/src-tauri/src/session_api.rs
Normal file
356
apps/desktop/src-tauri/src/session_api.rs
Normal file
@@ -0,0 +1,356 @@
|
||||
use anyhow::{Context, Result as AnyResult};
|
||||
use axum::{
|
||||
Router,
|
||||
body::{Body, to_bytes},
|
||||
extract::{Request, State as AxumState},
|
||||
http::{StatusCode as AxumStatusCode, header},
|
||||
response::Response,
|
||||
routing::{any, get, post},
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::local_vault::{
|
||||
LocalEntryQuery, bootstrap as vault_bootstrap, create_entry as vault_create_entry,
|
||||
create_secret as vault_create_secret, delete_entry as vault_delete_entry,
|
||||
delete_secret as vault_delete_secret, entry_detail as vault_entry_detail,
|
||||
list_entries as vault_list_entries, restore_entry as vault_restore_entry,
|
||||
reveal_secret_value as vault_reveal_secret_value, rollback_secret as vault_rollback_secret,
|
||||
secret_history as vault_secret_history, update_entry as vault_update_entry,
|
||||
update_secret as vault_update_secret,
|
||||
};
|
||||
use crate::{
|
||||
DesktopState, EntryDetail, EntryDraft, EntryListItem, EntryListQuery, SecretDraft,
|
||||
SecretUpdateDraft, current_device_token, map_entry_detail_to_local, map_entry_draft_to_local,
|
||||
map_local_entry_detail, map_local_history_item, map_local_secret_value,
|
||||
map_secret_draft_to_local, map_secret_update_to_local, split_secret_ref_for_ui,
|
||||
sync_local_vault,
|
||||
};
|
||||
|
||||
pub async fn desktop_session_health(
|
||||
AxumState(state): AxumState<DesktopState>,
|
||||
) -> Result<&'static str, AxumStatusCode> {
|
||||
current_device_token(&state)
|
||||
.map(|_| "ok")
|
||||
.map_err(|_| AxumStatusCode::UNAUTHORIZED)
|
||||
}
|
||||
|
||||
pub async fn desktop_session_api(
|
||||
AxumState(state): AxumState<DesktopState>,
|
||||
request: Request<Body>,
|
||||
) -> Response {
|
||||
let (parts, body) = request.into_parts();
|
||||
let path_and_query = parts
|
||||
.uri
|
||||
.path_and_query()
|
||||
.map(|value| value.as_str())
|
||||
.unwrap_or("/");
|
||||
|
||||
let body_bytes = match to_bytes(body, 1024 * 1024).await {
|
||||
Ok(bytes) => bytes,
|
||||
Err(_) => {
|
||||
return Response::builder()
|
||||
.status(AxumStatusCode::BAD_REQUEST)
|
||||
.body(Body::from("failed to read relay request body"))
|
||||
.expect("build relay bad request");
|
||||
}
|
||||
};
|
||||
|
||||
handle_local_session_request(&state, parts.method.as_str(), path_and_query, &body_bytes)
|
||||
.await
|
||||
.unwrap_or_else(|| {
|
||||
Response::builder()
|
||||
.status(AxumStatusCode::NOT_FOUND)
|
||||
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.body(Body::from(
|
||||
r#"{"error":"desktop local vault route not found"}"#,
|
||||
))
|
||||
.expect("build local session not found response")
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_local_session_request(
|
||||
state: &DesktopState,
|
||||
method: &str,
|
||||
path_and_query: &str,
|
||||
body_bytes: &[u8],
|
||||
) -> Option<Response> {
|
||||
let path = path_and_query.split('?').next().unwrap_or(path_and_query);
|
||||
let make_json = |status: AxumStatusCode, value: serde_json::Value| {
|
||||
Response::builder()
|
||||
.status(status)
|
||||
.header(header::CONTENT_TYPE, "application/json; charset=utf-8")
|
||||
.body(Body::from(value.to_string()))
|
||||
.expect("build local session response")
|
||||
};
|
||||
|
||||
match (method, path) {
|
||||
("GET", "/vault/status") => {
|
||||
let status = vault_bootstrap(&state.local_vault).await.ok()?;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::json!({
|
||||
"unlocked": status.unlocked,
|
||||
"has_master_password": status.has_master_password
|
||||
}),
|
||||
))
|
||||
}
|
||||
("GET", "/vault/entries") => {
|
||||
let url = format!("http://localhost{path_and_query}");
|
||||
let parsed = Url::parse(&url).ok()?;
|
||||
let mut query = EntryListQuery {
|
||||
folder: None,
|
||||
entry_type: None,
|
||||
query: None,
|
||||
deleted_only: false,
|
||||
};
|
||||
for (key, value) in parsed.query_pairs() {
|
||||
match key.as_ref() {
|
||||
"folder" => query.folder = Some(value.into_owned()),
|
||||
"entry_type" => query.entry_type = Some(value.into_owned()),
|
||||
"query" => query.query = Some(value.into_owned()),
|
||||
"deleted_only" => query.deleted_only = value == "true",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let entries = vault_list_entries(
|
||||
&state.local_vault,
|
||||
&LocalEntryQuery {
|
||||
folder: query.folder,
|
||||
cipher_type: query.entry_type,
|
||||
query: query.query,
|
||||
deleted_only: query.deleted_only,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(
|
||||
entries
|
||||
.into_iter()
|
||||
.map(|entry| EntryListItem {
|
||||
id: entry.id,
|
||||
title: entry.name,
|
||||
subtitle: entry.cipher_type,
|
||||
folder: entry.folder,
|
||||
deleted: entry.deleted,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.ok()?,
|
||||
))
|
||||
}
|
||||
_ if method == "GET" && path.starts_with("/vault/entries/") => {
|
||||
let entry_id = path.trim_start_matches("/vault/entries/");
|
||||
let detail = vault_entry_detail(&state.local_vault, entry_id)
|
||||
.await
|
||||
.ok()?;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(map_local_entry_detail(detail)).ok()?,
|
||||
))
|
||||
}
|
||||
("POST", "/vault/entries") => {
|
||||
let draft: EntryDraft = serde_json::from_slice(body_bytes).ok()?;
|
||||
let created = vault_create_entry(&state.local_vault, map_entry_draft_to_local(draft))
|
||||
.await
|
||||
.ok()?;
|
||||
let _ = sync_local_vault(state).await;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(map_local_entry_detail(created)).ok()?,
|
||||
))
|
||||
}
|
||||
_ if method == "PATCH" && path.starts_with("/vault/entries/") => {
|
||||
let entry_id = path.trim_start_matches("/vault/entries/").to_string();
|
||||
let mut detail: EntryDetail = serde_json::from_slice(body_bytes).ok()?;
|
||||
detail.id = entry_id;
|
||||
let updated = vault_update_entry(&state.local_vault, map_entry_detail_to_local(detail))
|
||||
.await
|
||||
.ok()?;
|
||||
let _ = sync_local_vault(state).await;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(map_local_entry_detail(updated)).ok()?,
|
||||
))
|
||||
}
|
||||
_ if method == "POST"
|
||||
&& path.starts_with("/vault/entries/")
|
||||
&& path.ends_with("/delete") =>
|
||||
{
|
||||
let entry_id = path
|
||||
.trim_start_matches("/vault/entries/")
|
||||
.trim_end_matches("/delete")
|
||||
.trim_end_matches('/');
|
||||
vault_delete_entry(&state.local_vault, entry_id)
|
||||
.await
|
||||
.ok()?;
|
||||
let _ = sync_local_vault(state).await;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::json!({ "ok": true }),
|
||||
))
|
||||
}
|
||||
_ if method == "POST"
|
||||
&& path.starts_with("/vault/entries/")
|
||||
&& path.ends_with("/restore") =>
|
||||
{
|
||||
let entry_id = path
|
||||
.trim_start_matches("/vault/entries/")
|
||||
.trim_end_matches("/restore")
|
||||
.trim_end_matches('/');
|
||||
vault_restore_entry(&state.local_vault, entry_id)
|
||||
.await
|
||||
.ok()?;
|
||||
let _ = sync_local_vault(state).await;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::json!({ "ok": true }),
|
||||
))
|
||||
}
|
||||
_ if method == "POST"
|
||||
&& path.starts_with("/vault/entries/")
|
||||
&& path.ends_with("/secrets") =>
|
||||
{
|
||||
let entry_id = path
|
||||
.trim_start_matches("/vault/entries/")
|
||||
.trim_end_matches("/secrets")
|
||||
.trim_end_matches('/');
|
||||
let secret: SecretDraft = serde_json::from_slice(body_bytes).ok()?;
|
||||
let updated = vault_create_secret(
|
||||
&state.local_vault,
|
||||
entry_id,
|
||||
map_secret_draft_to_local(secret),
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
let _ = sync_local_vault(state).await;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(map_local_entry_detail(updated)).ok()?,
|
||||
))
|
||||
}
|
||||
_ if method == "GET" && path.starts_with("/vault/secrets/") && path.ends_with("/value") => {
|
||||
let secret_id = path
|
||||
.trim_start_matches("/vault/secrets/")
|
||||
.trim_end_matches("/value")
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let (entry_id, secret_name) = split_secret_ref_for_ui(&secret_id).ok()?;
|
||||
let value = vault_reveal_secret_value(&state.local_vault, &entry_id, &secret_name)
|
||||
.await
|
||||
.ok()?;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(map_local_secret_value(value)).ok()?,
|
||||
))
|
||||
}
|
||||
_ if method == "GET"
|
||||
&& path.starts_with("/vault/secrets/")
|
||||
&& path.ends_with("/history") =>
|
||||
{
|
||||
let secret_id = path
|
||||
.trim_start_matches("/vault/secrets/")
|
||||
.trim_end_matches("/history")
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let (entry_id, secret_name) = split_secret_ref_for_ui(&secret_id).ok()?;
|
||||
let history = vault_secret_history(&state.local_vault, &entry_id, &secret_name)
|
||||
.await
|
||||
.ok()?;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(
|
||||
history
|
||||
.into_iter()
|
||||
.map(map_local_history_item)
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.ok()?,
|
||||
))
|
||||
}
|
||||
_ if method == "PATCH" && path.starts_with("/vault/secrets/") => {
|
||||
let secret_id = path.trim_start_matches("/vault/secrets/").to_string();
|
||||
let mut update: SecretUpdateDraft = serde_json::from_slice(body_bytes).ok()?;
|
||||
update.id = secret_id;
|
||||
let updated =
|
||||
vault_update_secret(&state.local_vault, map_secret_update_to_local(update))
|
||||
.await
|
||||
.ok()?;
|
||||
let _ = sync_local_vault(state).await;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(map_local_entry_detail(updated)).ok()?,
|
||||
))
|
||||
}
|
||||
_ if method == "POST"
|
||||
&& path.starts_with("/vault/secrets/")
|
||||
&& path.ends_with("/delete") =>
|
||||
{
|
||||
let secret_id = path
|
||||
.trim_start_matches("/vault/secrets/")
|
||||
.trim_end_matches("/delete")
|
||||
.trim_end_matches('/');
|
||||
vault_delete_secret(&state.local_vault, secret_id)
|
||||
.await
|
||||
.ok()?;
|
||||
let _ = sync_local_vault(state).await;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::json!({ "ok": true }),
|
||||
))
|
||||
}
|
||||
_ if method == "POST"
|
||||
&& path.starts_with("/vault/secrets/")
|
||||
&& path.ends_with("/rollback") =>
|
||||
{
|
||||
let secret_id = path
|
||||
.trim_start_matches("/vault/secrets/")
|
||||
.trim_end_matches("/rollback")
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
let payload: serde_json::Value = serde_json::from_slice(body_bytes).ok()?;
|
||||
let updated = vault_rollback_secret(
|
||||
&state.local_vault,
|
||||
&secret_id,
|
||||
payload.get("history_id").and_then(|value| value.as_i64()),
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
let _ = sync_local_vault(state).await;
|
||||
Some(make_json(
|
||||
AxumStatusCode::OK,
|
||||
serde_json::to_value(map_local_entry_detail(updated)).ok()?,
|
||||
))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_desktop_session_server(state: DesktopState) -> AnyResult<()> {
|
||||
let app = Router::new()
|
||||
.route("/healthz", get(desktop_session_health))
|
||||
.route("/vault/status", get(desktop_session_api))
|
||||
.route("/vault/entries", any(desktop_session_api))
|
||||
.route("/vault/entries/{id}", any(desktop_session_api))
|
||||
.route("/vault/entries/{id}/delete", post(desktop_session_api))
|
||||
.route("/vault/entries/{id}/restore", post(desktop_session_api))
|
||||
.route("/vault/entries/{id}/secrets", post(desktop_session_api))
|
||||
.route("/vault/secrets/{id}", any(desktop_session_api))
|
||||
.route("/vault/secrets/{id}/value", get(desktop_session_api))
|
||||
.route("/vault/secrets/{id}/history", get(desktop_session_api))
|
||||
.route("/vault/secrets/{id}/delete", post(desktop_session_api))
|
||||
.route("/vault/secrets/{id}/rollback", post(desktop_session_api))
|
||||
.with_state(state.clone());
|
||||
let listener = tokio::net::TcpListener::bind(&state.session_bind)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to bind desktop session relay {}",
|
||||
state.session_bind
|
||||
)
|
||||
})?;
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.context("desktop session relay server error")
|
||||
}
|
||||
31
apps/desktop/src-tauri/tauri.conf.json
Normal file
31
apps/desktop/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Secrets",
|
||||
"version": "3.0.0",
|
||||
"identifier": "dev.refining.secrets",
|
||||
"build": {
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": "",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "Secrets",
|
||||
"width": 420,
|
||||
"height": 400,
|
||||
"minWidth": 420,
|
||||
"minHeight": 400,
|
||||
"resizable": true,
|
||||
"titleBarStyle": "overlay",
|
||||
"hiddenTitle": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": false
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user