Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbc5639f99 | ||
|
|
754097f894 | ||
|
|
b761245fd0 |
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -910,7 +910,7 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||
|
||||
[[package]]
|
||||
name = "furumi-client-core"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -932,7 +932,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "furumi-common"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"prost",
|
||||
"protobuf-src",
|
||||
@@ -942,7 +942,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "furumi-mount-linux"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -959,7 +959,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "furumi-mount-macos"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -977,7 +977,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "furumi-server"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "furumi-client-core"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "furumi-common"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "furumi-mount-linux"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -75,7 +75,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
MountOption::NoExec, // Better security for media mount
|
||||
];
|
||||
|
||||
println!("Mounting Furumi-ng to {:?}", args.mount);
|
||||
println!("Mounting Furumi-ng v{} to {:?}", env!("CARGO_PKG_VERSION"), args.mount);
|
||||
|
||||
// Use Session + BackgroundSession for graceful unmount on exit
|
||||
let session = Session::new(fuse_fs, &args.mount, &options)?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "furumi-mount-macos"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -108,7 +108,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
println!("Mounted Furumi-ng to {:?}", mount_path);
|
||||
println!("Mounted Furumi-ng v{} to {:?}", env!("CARGO_PKG_VERSION"), mount_path);
|
||||
|
||||
// Wait for shutdown signal
|
||||
while running.load(Ordering::SeqCst) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "furumi-server"
|
||||
version = "0.3.1"
|
||||
version = "0.3.3"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -104,7 +104,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let svc = RemoteFileSystemServer::with_interceptor(remote_fs, auth.clone());
|
||||
|
||||
// Print startup info
|
||||
println!("Furumi-ng Server listening on {}", addr);
|
||||
println!("Furumi-ng Server v{} listening on {}", env!("CARGO_PKG_VERSION"), addr);
|
||||
if args.no_tls {
|
||||
println!("WARNING: TLS is DISABLED — traffic is unencrypted");
|
||||
} else {
|
||||
|
||||
@@ -31,14 +31,14 @@ pub fn token_hash(token: &str) -> String {
|
||||
format!("{:x}", h.finalize())
|
||||
}
|
||||
|
||||
/// axum middleware: if token is configured, requires a valid session cookie.
|
||||
pub async fn require_auth(
|
||||
State(state): State<WebState>,
|
||||
req: Request,
|
||||
mut req: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// Auth disabled when token is empty
|
||||
if state.token.is_empty() {
|
||||
req.extensions_mut().insert(super::AuthUserInfo("Unauthenticated".to_string()));
|
||||
return next.run(req).await;
|
||||
}
|
||||
|
||||
@@ -49,23 +49,24 @@ pub async fn require_auth(
|
||||
.unwrap_or("");
|
||||
|
||||
let expected = token_hash(&state.token);
|
||||
let mut authed = false;
|
||||
let mut authed_user = None;
|
||||
for c in cookies.split(';') {
|
||||
let c = c.trim();
|
||||
if let Some(val) = c.strip_prefix(&format!("{}=", SESSION_COOKIE)) {
|
||||
if val == expected {
|
||||
authed = true;
|
||||
authed_user = Some("Master Token".to_string());
|
||||
break;
|
||||
} else if let Some(oidc) = &state.oidc {
|
||||
if verify_sso_cookie(&oidc.session_secret, val) {
|
||||
authed = true;
|
||||
if let Some(user) = verify_sso_cookie(&oidc.session_secret, val) {
|
||||
authed_user = Some(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if authed {
|
||||
if let Some(user) = authed_user {
|
||||
req.extensions_mut().insert(super::AuthUserInfo(user));
|
||||
next.run(req).await
|
||||
} else {
|
||||
let uri = req.uri().path();
|
||||
@@ -86,10 +87,10 @@ pub fn generate_sso_cookie(secret: &[u8], user_id: &str) -> String {
|
||||
format!("sso:{}:{}", user_id, sig)
|
||||
}
|
||||
|
||||
pub fn verify_sso_cookie(secret: &[u8], cookie_val: &str) -> bool {
|
||||
pub fn verify_sso_cookie(secret: &[u8], cookie_val: &str) -> Option<String> {
|
||||
let parts: Vec<&str> = cookie_val.split(':').collect();
|
||||
if parts.len() != 3 || parts[0] != "sso" {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
let user_id = parts[1];
|
||||
let sig = parts[2];
|
||||
@@ -98,7 +99,11 @@ pub fn verify_sso_cookie(secret: &[u8], cookie_val: &str) -> bool {
|
||||
mac.update(user_id.as_bytes());
|
||||
|
||||
let expected_sig = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
|
||||
sig == expected_sig
|
||||
if sig == expected_sig {
|
||||
Some(user_id.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /login — show login form.
|
||||
@@ -180,6 +185,7 @@ pub async fn oidc_init(
|
||||
ClientId::new(client_id),
|
||||
Some(ClientSecret::new(client_secret)),
|
||||
)
|
||||
.set_auth_type(openidconnect::AuthType::RequestBody)
|
||||
.set_redirect_uri(RedirectUrl::new(redirect)?);
|
||||
|
||||
let mut session_secret = vec![0u8; 32];
|
||||
|
||||
@@ -53,6 +53,12 @@ pub fn build_router(root: PathBuf, token: String, oidc: Option<Arc<OidcState>>)
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
async fn player_html() -> axum::response::Html<&'static str> {
|
||||
axum::response::Html(include_str!("player.html"))
|
||||
#[derive(Clone)]
|
||||
pub struct AuthUserInfo(pub String);
|
||||
|
||||
async fn player_html(
|
||||
axum::extract::Extension(user_info): axum::extract::Extension<AuthUserInfo>,
|
||||
) -> axum::response::Html<String> {
|
||||
let html = include_str!("player.html").replace("<!-- USERNAME_PLACEHOLDER -->", &user_info.0);
|
||||
axum::response::Html(html)
|
||||
}
|
||||
|
||||
@@ -58,14 +58,30 @@ body {
|
||||
padding: 0.3rem 0.75rem; cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.btn-logout:hover { border-color: var(--danger); color: var(--danger); }
|
||||
.btn-menu {
|
||||
display: none;
|
||||
background: none; border: none; color: var(--text);
|
||||
font-size: 1.2rem; cursor: pointer; padding: 0.1rem 0.5rem;
|
||||
margin-right: 0.2rem; border-radius: 4px; transition: all 0.2s;
|
||||
}
|
||||
.btn-menu:hover { background: var(--bg-hover); }
|
||||
|
||||
/* ─── Main layout ─── */
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Mobile Overlay */
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.6); z-index: 20;
|
||||
}
|
||||
.sidebar-overlay.show { display: block; }
|
||||
|
||||
/* ─── File browser ─── */
|
||||
.sidebar {
|
||||
width: 280px;
|
||||
@@ -322,6 +338,30 @@ body {
|
||||
transition: all 0.25s; pointer-events: none; z-index: 100;
|
||||
}
|
||||
.toast.show { opacity: 1; transform: translateY(0); }
|
||||
|
||||
/* ─── Responsive (Mobile) ─── */
|
||||
@media (max-width: 768px) {
|
||||
.btn-menu { display: inline-block; }
|
||||
.header { padding: 0.75rem 1rem; }
|
||||
.sidebar {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0; left: -100%;
|
||||
width: 85%; max-width: 320px;
|
||||
z-index: 30;
|
||||
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 4px 0 20px rgba(0,0,0,0.6);
|
||||
}
|
||||
.sidebar.open { left: 0; }
|
||||
.queue-panel { flex: 1; min-width: 0; }
|
||||
.player-bar {
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto auto auto;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.np-info { display: grid; grid-template-columns: auto 1fr; text-align: left; }
|
||||
.volume-row { display: none; /* Hide volume on mobile to save space, rely on hardware buttons */ }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -329,19 +369,27 @@ body {
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-logo">
|
||||
<button class="btn-menu" id="btnMenu" onclick="toggleSidebar()">☰</button>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="9" cy="18" r="3"/><circle cx="18" cy="15" r="3"/>
|
||||
<path d="M12 18V6l9-3v3"/>
|
||||
</svg>
|
||||
Furumi Player
|
||||
</div>
|
||||
<button class="btn-logout" onclick="logout()">Sign out</button>
|
||||
<div style="display: flex; align-items: center; gap: 1rem;">
|
||||
<span style="font-size: 0.8rem; color: var(--text-dim);">
|
||||
<span style="opacity: 0.6; margin-right: 0.2rem;">👤</span>
|
||||
<!-- USERNAME_PLACEHOLDER -->
|
||||
</span>
|
||||
<button class="btn-logout" onclick="logout()">Sign out</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main -->
|
||||
<div class="main">
|
||||
<div class="sidebar-overlay" id="sidebarOverlay" onclick="toggleSidebar()"></div>
|
||||
<!-- Sidebar: file browser -->
|
||||
<aside class="sidebar">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<div class="sidebar-header">📁 Library</div>
|
||||
<div class="breadcrumb" id="breadcrumb">/ <span onclick="navigate('')">root</span></div>
|
||||
<div class="file-list" id="fileList">
|
||||
@@ -665,6 +713,13 @@ async function fetchMeta(path, idx) {
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const overlay = document.getElementById('sidebarOverlay');
|
||||
sidebar.classList.toggle('open');
|
||||
overlay.classList.toggle('show');
|
||||
}
|
||||
|
||||
function renderQueue() {
|
||||
const listEl = document.getElementById('queueList');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user