Files
channels-seerr/src/components/Layout/UserDropdown/index.tsx
Gauthier 4e48fdf2cb fix: rewrite avatarproxy and CachedImage (#1016)
* fix: rewrite avatarproxy and CachedImage

Avatar proxy was allowing every request to be proxied, no matter the original ressource's origin or
filetype. This PR fixes it be allowing only relevant resources to be cached, i.e. Jellyfin/Emby
images and TMDB images.

fix #1012, #1013

* fix: resolve CodeQL error

* fix: resolve CodeQL error

* fix: resolve review comments

* fix: resolve review comment

* fix: resolve CodeQL error

* fix: update imageproxy path
2024-10-17 21:24:15 +08:00

178 lines
6.5 KiB
TypeScript

import CachedImage from '@app/components/Common/CachedImage';
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
import { useUser } from '@app/hooks/useUser';
import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react';
import {
ArrowRightOnRectangleIcon,
ClockIcon,
} from '@heroicons/react/24/outline';
import { CogIcon, UserIcon } from '@heroicons/react/24/solid';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
import { forwardRef, Fragment } from 'react';
import { useIntl } from 'react-intl';
const messages = defineMessages('components.Layout.UserDropdown', {
myprofile: 'Profile',
settings: 'Settings',
requests: 'Requests',
signout: 'Sign Out',
});
const ForwardedLink = forwardRef<
HTMLAnchorElement,
LinkProps & React.ComponentPropsWithoutRef<'a'>
>(({ href, children, ...rest }, ref) => {
return (
<Link href={href} ref={ref} {...rest}>
{children}
</Link>
);
});
ForwardedLink.displayName = 'ForwardedLink';
const UserDropdown = () => {
const intl = useIntl();
const { user, revalidate } = useUser();
const logout = async () => {
const res = await fetch('/api/v1/auth/logout', {
method: 'POST',
});
if (!res.ok) throw new Error();
const data = await res.json();
if (data?.status === 'ok') {
revalidate();
}
};
return (
<Menu as="div" className="relative ml-3">
<div>
<Menu.Button
className="flex max-w-xs items-center rounded-full text-sm ring-1 ring-gray-700 hover:ring-gray-500 focus:outline-none focus:ring-gray-500"
data-testid="user-menu"
>
<CachedImage
type="avatar"
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user ? user.avatar : ''}
alt=""
width={40}
height={40}
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
appear
>
<Menu.Items className="absolute right-0 mt-2 w-72 origin-top-right rounded-md shadow-lg">
<div className="divide-y divide-gray-700 rounded-md bg-gray-800 bg-opacity-80 ring-1 ring-gray-700 backdrop-blur">
<div className="flex flex-col space-y-4 px-4 py-4">
<div className="flex items-center space-x-2">
<CachedImage
type="avatar"
className="h-8 w-8 rounded-full object-cover sm:h-10 sm:w-10"
src={user ? user.avatar : ''}
alt=""
width={40}
height={40}
/>
<div className="flex min-w-0 flex-col">
<span className="truncate text-xl font-semibold text-gray-200">
{user?.displayName}
</span>
{user?.displayName?.toLowerCase() !== user?.email && (
<span className="truncate text-sm text-gray-400">
{user?.email}
</span>
)}
</div>
</div>
{user && <MiniQuotaDisplay userId={user?.id} />}
</div>
<div className="p-1">
<Menu.Item>
{({ active }) => (
<ForwardedLink
href={`/profile`}
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: ''
}`}
data-testid="user-menu-profile"
>
<UserIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.myprofile)}</span>
</ForwardedLink>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<ForwardedLink
href={`/users/${user?.id}/requests?filter=all`}
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: ''
}`}
data-testid="user-menu-settings"
>
<ClockIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.requests)}</span>
</ForwardedLink>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<ForwardedLink
href={`/profile/settings`}
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: ''
}`}
data-testid="user-menu-settings"
>
<CogIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.settings)}</span>
</ForwardedLink>
)}
</Menu.Item>
<Menu.Item>
{({ active }) => (
<a
href="#"
className={`flex items-center rounded px-4 py-2 text-sm font-medium text-gray-200 transition duration-150 ease-in-out ${
active
? 'bg-gradient-to-br from-indigo-600 to-purple-600 text-white'
: ''
}`}
onClick={() => logout()}
>
<ArrowRightOnRectangleIcon className="mr-2 inline h-5 w-5" />
<span>{intl.formatMessage(messages.signout)}</span>
</a>
)}
</Menu.Item>
</div>
</div>
</Menu.Items>
</Transition>
</Menu>
);
};
export default UserDropdown;