mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
f2cf8a08df
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.
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
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 effect’s 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;
|
||
}
|