Files
Psychotoxical-psysonic/src/lib/hooks/useNavidromeAdminRole.ts
T
Psychotoxical f2cf8a08df refactor(hooks): move cross-cutting hooks into lib/hooks
The five residual src/hooks/ hooks (useConnectionStatus, useCardGridMetrics,
useTracklistColumns, useNavidromeAdminRole, useAnalysisPerfListener) are all
domain-agnostic — none imports @/features — so they join the other shared hooks
in src/lib/hooks/ (with their two colocated tests). Consumers repointed;
relative-up imports in the moved files become the @/ alias. src/hooks/ is removed.

Pure move + import updates, no behavior change. The layering baseline gains five
lib->store entries (the three hooks that read authStore/devOfflineBrowseStore are
only now caught under lib/) — same documented debt class as the api move.
2026-07-01 14:38:23 +02:00

85 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from 'react';
import { ndLogin } from '@/lib/api/navidromeAdmin';
import { useAuthStore } from '@/store/authStore';
import { isNavidromeServer } from '@/lib/server/subsonicServerIdentity';
export type NavidromeAdminRole = 'idle' | 'checking' | 'admin' | 'user' | 'na' | 'error';
/**
* Navidrome ≥ 0.62 restricts internet-radio management (create/update/delete) to
* admins (GHSA-jw24-qqrj-633c). Block those actions only for a *confirmed*
* standard Navidrome user; everything else — admin, non-Navidrome servers
* (`'na'`), and transient/unknown states — stays allowed, with the server as the
* final authority. Non-Navidrome servers never carried this restriction.
*/
export function canManageNavidromeRadio(role: NavidromeAdminRole): boolean {
return role !== 'user';
}
function normalizeServerUrl(url: string): string {
const withScheme = url.startsWith('http') ? url : `http://${url}`;
return withScheme.replace(/\/$/, '');
}
/**
* Probes Navidrome native login for the active server to learn whether the
* current Subsonic credentials belong to an admin account.
*/
export function useNavidromeAdminRole(): NavidromeAdminRole {
const isLoggedIn = useAuthStore(s => s.isLoggedIn);
const activeServerId = useAuthStore(s => s.activeServerId);
const server = useAuthStore(s => s.servers.find(srv => srv.id === s.activeServerId));
const identity = useAuthStore(s =>
activeServerId ? s.subsonicServerIdentityByServer[activeServerId] : undefined,
);
const [role, setRole] = useState<NavidromeAdminRole>('idle');
useEffect(() => {
if (!isLoggedIn || !server) {
// React Compiler set-state-in-effect rule: local state synced with store/prop inputs when the effects dependencies change.
// eslint-disable-next-line react-hooks/set-state-in-effect
setRole('na');
return;
}
if (!identity) {
setRole('checking');
return;
}
if (!isNavidromeServer(identity)) {
setRole('na');
return;
}
let cancelled = false;
setRole('checking');
const serverUrl = normalizeServerUrl(server.url);
ndLogin(serverUrl, server.username, server.password)
.then(res => {
if (cancelled) return;
setRole(res.isAdmin ? 'admin' : 'user');
})
.catch(() => {
if (!cancelled) setRole('error');
});
return () => {
cancelled = true;
};
// Keyed on the server's and identity's primitive fields; depending on the
// `server` / `identity` objects would re-probe the admin role on every render
// when their identities change but their fields do not.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
isLoggedIn,
activeServerId,
server?.id,
server?.url,
server?.username,
server?.password,
identity?.type,
identity?.serverVersion,
]);
return role;
}