refactor: checks if the default avatar is cached to avoid creating duplicates for different users
This commit is contained in:
@@ -343,6 +343,14 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
}),
|
||||
userType: UserType.EMBY,
|
||||
});
|
||||
|
||||
if (
|
||||
user.avatar.includes('https://gravatar.com') &&
|
||||
user.avatar.includes('default=mm&size=200')
|
||||
) {
|
||||
user.avatar = 'https://gravatar.com/avatar/?default=mm&size=200';
|
||||
}
|
||||
|
||||
break;
|
||||
case MediaServerType.JELLYFIN:
|
||||
settings.main.mediaServerType = MediaServerType.JELLYFIN;
|
||||
@@ -361,6 +369,14 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
}),
|
||||
userType: UserType.JELLYFIN,
|
||||
});
|
||||
|
||||
if (
|
||||
user.avatar.includes('https://gravatar.com') &&
|
||||
user.avatar.includes('default=mm&size=200')
|
||||
) {
|
||||
user.avatar = 'https://gravatar.com/avatar/?default=mm&size=200';
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
throw new Error('select_server_type');
|
||||
@@ -415,15 +431,23 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
}
|
||||
user.avatar = avatar;
|
||||
} else {
|
||||
const avatar = gravatarUrl(user.email || account.User.Name, {
|
||||
let avatar = gravatarUrl(user.email || account.User.Name, {
|
||||
default: 'mm',
|
||||
size: 200,
|
||||
});
|
||||
|
||||
if (
|
||||
avatar.includes('https://gravatar.com') &&
|
||||
avatar.includes('default=mm&size=200')
|
||||
) {
|
||||
avatar = 'https://gravatar.com/avatar/?default=mm&size=200';
|
||||
}
|
||||
|
||||
if (avatar !== user.avatar) {
|
||||
const avatarProxy = new ImageProxy('avatar', '');
|
||||
avatarProxy.clearCachedImage(user.avatar);
|
||||
}
|
||||
|
||||
user.avatar = avatar;
|
||||
}
|
||||
user.jellyfinUsername = account.User.Name;
|
||||
@@ -474,6 +498,13 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
||||
? UserType.JELLYFIN
|
||||
: UserType.EMBY,
|
||||
});
|
||||
|
||||
if (
|
||||
user.avatar.includes('https://gravatar.com') &&
|
||||
user.avatar.includes('default=mm&size=200')
|
||||
) {
|
||||
user.avatar = 'https://gravatar.com/avatar/?default=mm&size=200';
|
||||
}
|
||||
//initialize Jellyfin/Emby users with local login
|
||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||
if (passedExplicitPassword) {
|
||||
|
||||
@@ -7,8 +7,16 @@ const router = Router();
|
||||
const avatarImageProxy = new ImageProxy('avatar', '');
|
||||
// Proxy avatar images
|
||||
router.get('/*', async (req, res) => {
|
||||
const imagePath = req.url.startsWith('/') ? req.url.slice(1) : req.url;
|
||||
let imagePath = req.url.startsWith('/') ? req.url.slice(1) : req.url;
|
||||
|
||||
try {
|
||||
if (
|
||||
imagePath.includes('https://gravatar.com') &&
|
||||
imagePath.includes('default=mm&size=200')
|
||||
) {
|
||||
imagePath = 'https://gravatar.com/avatar/?default=mm&size=200';
|
||||
}
|
||||
|
||||
const imageData = await avatarImageProxy.getImage(imagePath);
|
||||
|
||||
res.writeHead(200, {
|
||||
|
||||
@@ -122,7 +122,14 @@ router.post(
|
||||
}
|
||||
|
||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
||||
const avatar = gravatarUrl(email, { default: 'mm', size: 200 });
|
||||
let avatar = gravatarUrl(email, { default: 'mm', size: 200 });
|
||||
|
||||
if (
|
||||
avatar.includes('https://gravatar.com') &&
|
||||
avatar.includes('default=mm&size=200')
|
||||
) {
|
||||
avatar = 'https://gravatar.com/avatar/?default=mm&size=200';
|
||||
}
|
||||
|
||||
if (
|
||||
!passedExplicitPassword &&
|
||||
@@ -557,6 +564,13 @@ router.post(
|
||||
: UserType.EMBY,
|
||||
});
|
||||
|
||||
if (
|
||||
newUser.avatar.includes('https://gravatar.com') &&
|
||||
newUser.avatar.includes('default=mm&size=200')
|
||||
) {
|
||||
newUser.avatar = 'https://gravatar.com/avatar/?default=mm&size=200';
|
||||
}
|
||||
|
||||
await userRepository.save(newUser);
|
||||
createdUsers.push(newUser);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import type Issue from '@server/entity/Issue';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useState } from 'react';
|
||||
@@ -264,8 +263,9 @@ const IssueDetails = () => {
|
||||
</div>
|
||||
<h1>
|
||||
<Link
|
||||
href={`/${issueData.media.mediaType === MediaType.MOVIE ? 'movie' : 'tv'
|
||||
}/${data.id}`}
|
||||
href={`/${
|
||||
issueData.media.mediaType === MediaType.MOVIE ? 'movie' : 'tv'
|
||||
}/${data.id}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{title}
|
||||
@@ -302,7 +302,7 @@ const IssueDetails = () => {
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(issueData.createdAt).getTime() - Date.now()) /
|
||||
1000
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
@@ -367,7 +367,7 @@ const IssueDetails = () => {
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(issueData.updatedAt).getTime() - Date.now()) /
|
||||
1000
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
@@ -388,16 +388,16 @@ const IssueDetails = () => {
|
||||
<PlayIcon />
|
||||
<span>
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? intl.formatMessage(messages.playonplex, {
|
||||
? intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName: 'Plex',
|
||||
})
|
||||
: intl.formatMessage(messages.playonplex, {
|
||||
: intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
</span>
|
||||
@@ -436,16 +436,16 @@ const IssueDetails = () => {
|
||||
<PlayIcon />
|
||||
<span>
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.play4konplex, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? intl.formatMessage(messages.play4konplex, {
|
||||
? intl.formatMessage(messages.play4konplex, {
|
||||
mediaServerName: 'Plex',
|
||||
})
|
||||
: intl.formatMessage(messages.play4konplex, {
|
||||
: intl.formatMessage(messages.play4konplex, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
</span>
|
||||
@@ -530,52 +530,52 @@ const IssueDetails = () => {
|
||||
<div className="mt-4 flex items-center justify-end space-x-2">
|
||||
{(hasPermission(Permission.MANAGE_ISSUES) ||
|
||||
belongsToUser) && (
|
||||
<>
|
||||
{issueData.status === IssueStatus.OPEN ? (
|
||||
<Button
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
onClick={async () => {
|
||||
await updateIssueStatus('resolved');
|
||||
<>
|
||||
{issueData.status === IssueStatus.OPEN ? (
|
||||
<Button
|
||||
type="button"
|
||||
buttonType="danger"
|
||||
onClick={async () => {
|
||||
await updateIssueStatus('resolved');
|
||||
|
||||
if (values.message) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
values.message
|
||||
? messages.closeissueandcomment
|
||||
: messages.closeissue
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
buttonType="default"
|
||||
onClick={async () => {
|
||||
await updateIssueStatus('open');
|
||||
if (values.message) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
values.message
|
||||
? messages.closeissueandcomment
|
||||
: messages.closeissue
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="button"
|
||||
buttonType="default"
|
||||
onClick={async () => {
|
||||
await updateIssueStatus('open');
|
||||
|
||||
if (values.message) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowPathIcon />
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
values.message
|
||||
? messages.reopenissueandcomment
|
||||
: messages.reopenissue
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
if (values.message) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ArrowPathIcon />
|
||||
<span>
|
||||
{intl.formatMessage(
|
||||
values.message
|
||||
? messages.reopenissueandcomment
|
||||
: messages.reopenissue
|
||||
)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
buttonType="primary"
|
||||
@@ -641,7 +641,7 @@ const IssueDetails = () => {
|
||||
<FormattedRelativeTime
|
||||
value={Math.floor(
|
||||
(new Date(issueData.updatedAt).getTime() - Date.now()) /
|
||||
1000
|
||||
1000
|
||||
)}
|
||||
updateIntervalInSeconds={1}
|
||||
numeric="auto"
|
||||
@@ -662,16 +662,16 @@ const IssueDetails = () => {
|
||||
<PlayIcon />
|
||||
<span>
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? intl.formatMessage(messages.playonplex, {
|
||||
? intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName: 'Plex',
|
||||
})
|
||||
: intl.formatMessage(messages.playonplex, {
|
||||
: intl.formatMessage(messages.playonplex, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
</span>
|
||||
@@ -709,16 +709,16 @@ const IssueDetails = () => {
|
||||
<PlayIcon />
|
||||
<span>
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.play4konplex, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? intl.formatMessage(messages.play4konplex, {
|
||||
? intl.formatMessage(messages.play4konplex, {
|
||||
mediaServerName: 'Plex',
|
||||
})
|
||||
: intl.formatMessage(messages.play4konplex, {
|
||||
: intl.formatMessage(messages.play4konplex, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
</span>
|
||||
|
||||
@@ -28,7 +28,6 @@ import type { MediaWatchDataResponse } from '@server/interfaces/api/mediaInterfa
|
||||
import type { RadarrSettings, SonarrSettings } from '@server/lib/settings';
|
||||
import type { MovieDetails } from '@server/models/Movie';
|
||||
import type { TvDetails } from '@server/models/Tv';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
@@ -314,8 +313,8 @@ const ManageSlideOver = ({
|
||||
{!!watchData?.data && (
|
||||
<div
|
||||
className={`grid grid-cols-1 divide-y divide-gray-700 overflow-hidden border-gray-700 text-sm text-gray-300 shadow ${data.mediaInfo?.tautulliUrl
|
||||
? 'rounded-t-md border-x border-t'
|
||||
: 'rounded-md border'
|
||||
? 'rounded-t-md border-x border-t'
|
||||
: 'rounded-md border'
|
||||
}`}
|
||||
>
|
||||
<div className="grid grid-cols-3 divide-x divide-gray-700">
|
||||
@@ -471,8 +470,8 @@ const ManageSlideOver = ({
|
||||
{watchData?.data4k && (
|
||||
<div
|
||||
className={`grid grid-cols-1 divide-y divide-gray-700 overflow-hidden border-gray-700 text-sm text-gray-300 shadow ${data.mediaInfo?.tautulliUrl4k
|
||||
? 'rounded-t-md border-x border-t'
|
||||
: 'rounded-md border'
|
||||
? 'rounded-t-md border-x border-t'
|
||||
: 'rounded-md border'
|
||||
}`}
|
||||
>
|
||||
<div className="grid grid-cols-3 divide-x divide-gray-700">
|
||||
|
||||
@@ -6,7 +6,6 @@ import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { MediaServerType } from '@server/constants/server';
|
||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import Image from 'next/image';
|
||||
import { useState } from 'react';
|
||||
import { useIntl } from 'react-intl';
|
||||
import { useToasts } from 'react-toast-notifications';
|
||||
@@ -158,7 +157,7 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
title={intl.formatMessage(messages.newJellyfinsigninenabled, {
|
||||
mediaServerName:
|
||||
settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
MediaServerType.EMBY
|
||||
? 'Emby'
|
||||
: 'Jellyfin',
|
||||
strong: (msg: React.ReactNode) => (
|
||||
@@ -190,13 +189,15 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${isAllUsers() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
className={`${
|
||||
isAllUsers() ? 'bg-indigo-500' : 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${isAllUsers() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
className={`${
|
||||
isAllUsers() ? 'translate-x-5' : 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</th>
|
||||
@@ -230,17 +231,19 @@ const JellyfinImportModal: React.FC<JellyfinImportProps> = ({
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${isSelectedUser(user.id)
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
className={`${
|
||||
isSelectedUser(user.id)
|
||||
? 'bg-indigo-500'
|
||||
: 'bg-gray-800'
|
||||
} absolute mx-auto h-4 w-9 rounded-full transition-colors duration-200 ease-in-out`}
|
||||
></span>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`${isSelectedUser(user.id)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
className={`${
|
||||
isSelectedUser(user.id)
|
||||
? 'translate-x-5'
|
||||
: 'translate-x-0'
|
||||
} absolute left-0 inline-block h-5 w-5 transform rounded-full border border-gray-200 bg-white shadow transition-transform duration-200 ease-in-out group-focus:border-blue-300 group-focus:ring`}
|
||||
></span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
@@ -29,7 +29,6 @@ import { MediaServerType } from '@server/constants/server';
|
||||
import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces';
|
||||
import { hasPermission } from '@server/lib/permissions';
|
||||
import { Field, Form, Formik } from 'formik';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
@@ -104,7 +103,8 @@ const UserList = () => {
|
||||
error,
|
||||
mutate: revalidate,
|
||||
} = useSWR<UserResultsResponse>(
|
||||
`/api/v1/user?take=${currentPageSize}&skip=${pageIndex * currentPageSize
|
||||
`/api/v1/user?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&sort=${currentSort}`
|
||||
);
|
||||
|
||||
@@ -215,9 +215,9 @@ const UserList = () => {
|
||||
!value
|
||||
? Yup.string()
|
||||
: Yup.string().min(
|
||||
8,
|
||||
intl.formatMessage(messages.validationpasswordminchars)
|
||||
)
|
||||
8,
|
||||
intl.formatMessage(messages.validationpasswordminchars)
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
@@ -406,8 +406,9 @@ const UserList = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`form-row ${passwordGenerationEnabled ? '' : 'opacity-50'
|
||||
}`}
|
||||
className={`form-row ${
|
||||
passwordGenerationEnabled ? '' : 'opacity-50'
|
||||
}`}
|
||||
>
|
||||
<label htmlFor="genpassword" className="checkbox-label">
|
||||
{intl.formatMessage(messages.autogeneratepassword)}
|
||||
@@ -426,8 +427,9 @@ const UserList = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`form-row ${values.genpassword ? 'opacity-50' : ''
|
||||
}`}
|
||||
className={`form-row ${
|
||||
values.genpassword ? 'opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
<label htmlFor="password" className="text-label">
|
||||
{intl.formatMessage(messages.password)}
|
||||
@@ -532,16 +534,16 @@ const UserList = () => {
|
||||
<InboxArrowDownIcon />
|
||||
<span>
|
||||
{settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.EMBY
|
||||
MediaServerType.EMBY
|
||||
? intl.formatMessage(messages.importfrommediaserver, {
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
mediaServerName: 'Emby',
|
||||
})
|
||||
: settings.currentSettings.mediaServerType ===
|
||||
MediaServerType.PLEX
|
||||
? intl.formatMessage(messages.importfrommediaserver, {
|
||||
? intl.formatMessage(messages.importfrommediaserver, {
|
||||
mediaServerName: 'Plex',
|
||||
})
|
||||
: intl.formatMessage(messages.importfrommediaserver, {
|
||||
: intl.formatMessage(messages.importfrommediaserver, {
|
||||
mediaServerName: 'Jellyfin',
|
||||
})}
|
||||
</span>
|
||||
@@ -655,19 +657,19 @@ const UserList = () => {
|
||||
user.jellyfinUsername ||
|
||||
user.plexUsername
|
||||
)?.toLowerCase() !== user.email && (
|
||||
<div className="text-sm leading-5 text-gray-300">
|
||||
{user.email}
|
||||
</div>
|
||||
)}
|
||||
<div className="text-sm leading-5 text-gray-300">
|
||||
{user.email}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
{user.id === currentUser?.id ||
|
||||
currentHasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
) ? (
|
||||
currentHasPermission(
|
||||
[Permission.MANAGE_REQUESTS, Permission.REQUEST_VIEW],
|
||||
{ type: 'or' }
|
||||
) ? (
|
||||
<Link
|
||||
href={`/users/${user.id}/requests`}
|
||||
className="text-sm leading-5 transition duration-300 hover:underline"
|
||||
@@ -705,8 +707,8 @@ const UserList = () => {
|
||||
{user.id === 1
|
||||
? intl.formatMessage(messages.owner)
|
||||
: hasPermission(Permission.ADMIN, user.permissions)
|
||||
? intl.formatMessage(messages.admin)
|
||||
: intl.formatMessage(messages.user)}
|
||||
? intl.formatMessage(messages.admin)
|
||||
: intl.formatMessage(messages.user)}
|
||||
</Table.TD>
|
||||
<Table.TD>
|
||||
{intl.formatDate(user.createdAt, {
|
||||
|
||||
Reference in New Issue
Block a user