mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
Add manual-import and download-access features
Introduce manual import workflow and download permission support. Adds a Prisma migration and schema field (users.download_access) to track per-user download access, and updates admin UI to toggle global and per-user download access. Implements new APIs: filesystem browse, manual-import endpoint, download-access settings, audiobook download-status, and on-demand download-token generation. Adds frontend components for manual import and related tests, plus documentation for the manual-import feature and the documentation-agent prompt. Key files: prisma/migrations/20260212000000_add_download_access_permission/migration.sql, prisma/schema.prisma, src/app/api/admin/filesystem/browse/route.ts, src/app/api/admin/manual-import/route.ts, src/app/api/admin/settings/download-access/route.ts, src/app/api/requests/[id]/download-token/route.ts, src/app/api/audiobooks/[asin]/download-status/route.ts, and updated admin users pages/components and permissions util.
This commit is contained in:
@@ -28,6 +28,7 @@ interface User {
|
||||
lastLoginAt: string | null;
|
||||
autoApproveRequests: boolean | null;
|
||||
interactiveSearchAccess: boolean | null;
|
||||
downloadAccess: boolean | null;
|
||||
_count: {
|
||||
requests: number;
|
||||
};
|
||||
@@ -193,6 +194,10 @@ function AdminUsersPageContent() {
|
||||
'/api/admin/settings/interactive-search',
|
||||
authenticatedFetcher
|
||||
);
|
||||
const { data: globalDownloadAccessData, mutate: mutateGlobalDownloadAccess } = useSWR(
|
||||
'/api/admin/settings/download-access',
|
||||
authenticatedFetcher
|
||||
);
|
||||
const [editDialog, setEditDialog] = useState<{
|
||||
isOpen: boolean;
|
||||
user: User | null;
|
||||
@@ -212,6 +217,7 @@ function AdminUsersPageContent() {
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [globalAutoApprove, setGlobalAutoApprove] = useState<boolean>(false);
|
||||
const [globalInteractiveSearch, setGlobalInteractiveSearch] = useState<boolean>(true);
|
||||
const [globalDownloadAccess, setGlobalDownloadAccess] = useState<boolean>(true);
|
||||
const [globalSettingsOpen, setGlobalSettingsOpen] = useState(false);
|
||||
const [permissionsUserId, setPermissionsUserId] = useState<string | null>(null);
|
||||
const toast = useToast();
|
||||
@@ -237,6 +243,15 @@ function AdminUsersPageContent() {
|
||||
}
|
||||
}, [globalInteractiveSearchData]);
|
||||
|
||||
// Sync global download access state (default to true if not set)
|
||||
useEffect(() => {
|
||||
if (globalDownloadAccessData?.downloadAccess !== undefined) {
|
||||
setGlobalDownloadAccess(globalDownloadAccessData.downloadAccess);
|
||||
} else if (globalDownloadAccessData !== undefined && globalDownloadAccessData.downloadAccess === undefined) {
|
||||
setGlobalDownloadAccess(true);
|
||||
}
|
||||
}, [globalDownloadAccessData]);
|
||||
|
||||
const handleGlobalAutoApproveToggle = async (newValue: boolean) => {
|
||||
setGlobalAutoApprove(newValue);
|
||||
try {
|
||||
@@ -311,6 +326,43 @@ function AdminUsersPageContent() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleGlobalDownloadAccessToggle = async (newValue: boolean) => {
|
||||
setGlobalDownloadAccess(newValue);
|
||||
try {
|
||||
await fetchJSON('/api/admin/settings/download-access', {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ downloadAccess: newValue }),
|
||||
});
|
||||
toast.success(`Global download access ${newValue ? 'enabled' : 'disabled'}`);
|
||||
mutateGlobalDownloadAccess();
|
||||
mutate();
|
||||
} catch (err) {
|
||||
setGlobalDownloadAccess(!newValue);
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to update download access setting';
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUserDownloadAccessToggle = async (user: User, newValue: boolean) => {
|
||||
const previousUsers = data?.users || [];
|
||||
const optimisticUsers = previousUsers.map((u: User) =>
|
||||
u.id === user.id ? { ...u, downloadAccess: newValue } : u
|
||||
);
|
||||
mutate({ users: optimisticUsers }, false);
|
||||
try {
|
||||
await fetchJSON(`/api/admin/users/${user.id}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ role: user.role, downloadAccess: newValue }),
|
||||
});
|
||||
toast.success(`Download access ${newValue ? 'enabled' : 'disabled'} for ${user.plexUsername}`);
|
||||
mutate();
|
||||
} catch (err) {
|
||||
mutate({ users: previousUsers }, false);
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to update user download access setting';
|
||||
toast.error(errorMsg);
|
||||
}
|
||||
};
|
||||
|
||||
const showEditDialog = (user: User) => {
|
||||
setEditRole(user.role);
|
||||
setEditDialog({ isOpen: true, user });
|
||||
@@ -909,6 +961,8 @@ function AdminUsersPageContent() {
|
||||
onToggleAutoApprove={handleGlobalAutoApproveToggle}
|
||||
globalInteractiveSearch={globalInteractiveSearch}
|
||||
onToggleInteractiveSearch={handleGlobalInteractiveSearchToggle}
|
||||
globalDownloadAccess={globalDownloadAccess}
|
||||
onToggleDownloadAccess={handleGlobalDownloadAccessToggle}
|
||||
/>
|
||||
|
||||
{/* User Permissions Modal */}
|
||||
@@ -918,12 +972,16 @@ function AdminUsersPageContent() {
|
||||
user={permissionsUser}
|
||||
globalAutoApprove={globalAutoApprove}
|
||||
globalInteractiveSearch={globalInteractiveSearch}
|
||||
globalDownloadAccess={globalDownloadAccess}
|
||||
onToggleAutoApprove={(user, newValue) => {
|
||||
handleUserAutoApproveToggle(user as User, newValue);
|
||||
}}
|
||||
onToggleInteractiveSearch={(user, newValue) => {
|
||||
handleUserInteractiveSearchToggle(user as User, newValue);
|
||||
}}
|
||||
onToggleDownloadAccess={(user, newValue) => {
|
||||
handleUserDownloadAccessToggle(user as User, newValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user