feat: multi-user reading progress & backoffice impersonation
- Scope all reading progress (books, series, stats) by user via Option<Extension<AuthUser>> — admin sees aggregate, read token sees own data - Fix duplicate book rows when admin views lists (IS NOT NULL guard on JOIN) - Add X-As-User header support: admin can impersonate any user from backoffice - UserSwitcher dropdown in nav header (persisted via as_user_id cookie) - Per-user filter pills on "Currently reading" and "Recently read" dashboard sections - Inline username editing (UsernameEdit component with optimistic update) - PATCH /admin/users/:id endpoint to rename a user - Unassigned read tokens row in users table - Komga sync now requires a user_id — reading progress attributed to selected user - Migration 0051: add user_id column to komga_sync_reports - Nav breakpoints: icons-only from md, labels from xl, hamburger until md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
38
apps/backoffice/app/components/TokenUserSelect.tsx
Normal file
38
apps/backoffice/app/components/TokenUserSelect.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
"use client";
|
||||
|
||||
import { useOptimistic, useTransition } from "react";
|
||||
|
||||
interface TokenUserSelectProps {
|
||||
tokenId: string;
|
||||
currentUserId?: string;
|
||||
users: { id: string; username: string }[];
|
||||
action: (formData: FormData) => Promise<void>;
|
||||
noUserLabel: string;
|
||||
}
|
||||
|
||||
export function TokenUserSelect({ tokenId, currentUserId, users, action, noUserLabel }: TokenUserSelectProps) {
|
||||
const [optimisticValue, setOptimisticValue] = useOptimistic(currentUserId ?? "");
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
return (
|
||||
<select
|
||||
value={optimisticValue}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
startTransition(async () => {
|
||||
setOptimisticValue(newValue);
|
||||
const fd = new FormData();
|
||||
fd.append("id", tokenId);
|
||||
fd.append("user_id", newValue);
|
||||
await action(fd);
|
||||
});
|
||||
}}
|
||||
className="flex h-8 rounded-md border border-input bg-background px-2 py-0 text-xs shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
<option value="">{noUserLabel}</option>
|
||||
{users.map((u) => (
|
||||
<option key={u.id} value={u.id}>{u.username}</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user