From afa40a88c5eebb0aec07c1a4b662104afadc79b5 Mon Sep 17 00:00:00 2001
From: Parsifa1
Date: Sat, 14 Feb 2026 14:22:24 +0000
Subject: [PATCH] feat(icons): impl /icons/*
---
src/handlers/icons.rs | 63 +++++++++++++++++++++++++++++++++++++++++++
src/handlers/mod.rs | 1 +
src/router.rs | 3 ++-
wrangler.jsonc | 2 +-
4 files changed, 67 insertions(+), 2 deletions(-)
create mode 100644 src/handlers/icons.rs
diff --git a/src/handlers/icons.rs b/src/handlers/icons.rs
new file mode 100644
index 00000000..92b49043
--- /dev/null
+++ b/src/handlers/icons.rs
@@ -0,0 +1,63 @@
+use axum::{
+ extract::{Path, State},
+ http::StatusCode,
+ response::Response,
+};
+use std::sync::Arc;
+use worker::Env;
+
+use crate::error::AppError;
+
+#[worker::send]
+pub async fn get_icon(
+ State(_env): State>,
+ Path(path): Path,
+) -> Result {
+ let domain = path
+ .strip_suffix("/icon.png")
+ .unwrap_or(&path)
+ .to_string();
+
+ let target_url = format!("https://vault.bitwarden.com/icons/{}/icon.png", domain);
+
+ let request = worker::Request::new(&target_url, worker::Method::Get)
+ .map_err(|_| AppError::Internal)?;
+
+ let mut upstream_response = worker::Fetch::Request(request)
+ .send()
+ .await
+ .map_err(|e| {
+ log::error!("Failed to fetch icon from Bitwarden: {:?}", e);
+ AppError::Internal
+ })?;
+
+ let status = upstream_response.status_code();
+
+ let body_bytes = upstream_response
+ .bytes()
+ .await
+ .map_err(|e| {
+ log::error!("Failed to read icon response body: {:?}", e);
+ AppError::Internal
+ })?;
+
+ let mut response = Response::new(axum::body::Body::from(body_bytes));
+ *response.status_mut() = StatusCode::from_u16(status)
+ .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
+
+ if let Ok(Some(content_type)) = upstream_response.headers().get("content-type") {
+ if let Ok(header_value) = axum::http::HeaderValue::from_str(&content_type) {
+ response.headers_mut().insert(
+ axum::http::header::CONTENT_TYPE,
+ header_value,
+ );
+ }
+ }
+
+ response.headers_mut().insert(
+ axum::http::header::CACHE_CONTROL,
+ axum::http::HeaderValue::from_static("public, max-age=604800, immutable"),
+ );
+
+ Ok(response)
+}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index 151de1d2..5a32140d 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -9,3 +9,4 @@ pub mod two_factor;
pub mod devices;
pub mod sends;
pub mod usage;
+pub mod icons;
diff --git a/src/router.rs b/src/router.rs
index fcd56a56..89e9d892 100644
--- a/src/router.rs
+++ b/src/router.rs
@@ -7,13 +7,14 @@ use axum::extract::DefaultBodyLimit;
use std::sync::Arc;
use worker::Env;
-use crate::handlers::{accounts, ciphers, config, identity, sync, folders, import, two_factor, devices, sends, usage};
+use crate::handlers::{accounts, ciphers, config, identity, sync, folders, import, two_factor, devices, sends, usage, icons};
pub fn api_router(env: Env) -> Router {
let app_state = Arc::new(env);
Router::new()
.route("/demo.html", get(|| async { Html(include_str!("../static/demo.html")) }))
+ .route("/icons/{*path}", get(icons::get_icon))
// Identity/Auth routes
.route("/identity/accounts/prelogin", post(accounts::prelogin))
.route("/api/accounts/prelogin", post(accounts::prelogin))
diff --git a/wrangler.jsonc b/wrangler.jsonc
index 54183c1a..095bafbf 100644
--- a/wrangler.jsonc
+++ b/wrangler.jsonc
@@ -30,6 +30,6 @@
"assets": {
"directory": "./static/web-vault",
"not_found_handling": "single-page-application",
- "run_worker_first": ["/api/*", "/identity/*", "/sends/*", "/demo.html"]
+ "run_worker_first": ["/api/*", "/identity/*", "/sends/*", "/demo.html", "/icons/*"]
}
}