3 Commits

Author SHA1 Message Date
Ultradesu
cbc5639f99 Fixed UI
All checks were successful
Build and Publish Deb Package / build-deb (push) Successful in 47s
Publish Server Image / build-and-push-image (push) Successful in 2m19s
2026-03-17 15:17:30 +00:00
Ultradesu
754097f894 Fixed OIDC
All checks were successful
Build and Publish Deb Package / build-deb (push) Successful in 2m10s
Publish Server Image / build-and-push-image (push) Successful in 4m43s
2026-03-17 15:04:04 +00:00
Ultradesu
b761245fd0 Fixed OIDC 2026-03-17 15:03:36 +00:00
12 changed files with 94 additions and 27 deletions

10
Cargo.lock generated
View File

@@ -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",

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-client-core"
version = "0.3.1"
version = "0.3.3"
edition = "2024"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-common"
version = "0.3.1"
version = "0.3.3"
edition = "2024"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-mount-linux"
version = "0.3.1"
version = "0.3.3"
edition = "2024"
[dependencies]

View File

@@ -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)?;

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-mount-macos"
version = "0.3.1"
version = "0.3.3"
edition = "2024"
[dependencies]

View File

@@ -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) {

View File

@@ -1,6 +1,6 @@
[package]
name = "furumi-server"
version = "0.3.1"
version = "0.3.3"
edition = "2024"
[dependencies]

View File

@@ -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 {

View File

@@ -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];

View File

@@ -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)
}

View File

@@ -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');