mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2025-12-24 02:39:18 -05:00
Compare commits
10 Commits
4e9ba75377
...
0xsysr3ll/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3070ba74ae | ||
|
|
8979085fb5 | ||
|
|
4232ef19d4 | ||
|
|
db48f449f5 | ||
|
|
37b83fe56d | ||
|
|
3ee69663dc | ||
|
|
539d49879d | ||
|
|
15356dfe49 | ||
|
|
1f04eeb040 | ||
|
|
e3028c21f2 |
@@ -8,7 +8,7 @@
|
||||
<p align="center">
|
||||
<a href="https://discord.gg/seerr"><img src="https://img.shields.io/discord/783137440809746482" alt="Discord"></a>
|
||||
<a href="https://hub.docker.com/r/seerr/seerr"><img src="https://img.shields.io/docker/pulls/seerr/seerr" alt="Docker pulls"></a>
|
||||
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/seerr-frontend/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://translate.seerr.dev/engage/seerr/"><img src="https://translate.seerr.dev/widget/seerr/svg-badge.svg" alt="Translation status" /></a>
|
||||
<a href="https://github.com/seerr-team/seerr/blob/develop/LICENSE"><img alt="GitHub" src="https://img.shields.io/github/license/seerr-team/seerr"></a>
|
||||
|
||||
**Seerr** is a free and open source software application for managing requests for your media library. It integrates with the media server of your choice: [Jellyfin](https://jellyfin.org), [Plex](https://plex.tv), and [Emby](https://emby.media/). In addition, it integrates with your existing services, such as **[Sonarr](https://sonarr.tv/)**, **[Radarr](https://radarr.video/)**.
|
||||
|
||||
@@ -174,4 +174,36 @@ This can happen if you have a new installation of Jellyfin/Emby or if you have c
|
||||
|
||||
This process should restore your admin privileges while preserving your settings.
|
||||
|
||||
## Failed to enable web push notifications
|
||||
|
||||
### Option 1: You are using Pi-hole
|
||||
|
||||
When using Pi-hole, you need to whitelist the proper domains in order for the queries to not be intercepted and blocked by Pi-hole.
|
||||
If you are using a chromium based browser (eg: Chrome, Brave, Edge...), the domain you need to whitelist is `fcm.googleapis.com`
|
||||
If you are using Firefox, the domain you need to whitelist is `push.services.mozilla.com`
|
||||
|
||||
1. Log into your Pi-hole through the admin interface, then click on Domains situated under GROUP MANAGEMENT.
|
||||
2. Add the domain corresponding to your browser in the `Domain to be added` field and then click on Add to allowed domains.
|
||||
3. Now in order for those changes to be used you need to flush your current dns cache.
|
||||
4. You can do so by using this command line in your Pi-hole terminal:
|
||||
```bash
|
||||
pihole restartdns
|
||||
```
|
||||
If this command fails (which is unlikely), use this equivalent:
|
||||
```bash
|
||||
pihole -f && pihole restartdns
|
||||
```
|
||||
5. Then restart your Seerr instance and try to enable the web push notifications again.
|
||||
|
||||
|
||||
### Option 2: You are using Brave browser
|
||||
|
||||
Brave is a "De-Googled" browser. So by default or if you refused a prompt in the past, it cuts the access to the FCM (Firebase Cloud Messaging) service, which is mandatory for the web push notifications on Chromium based browsers.
|
||||
|
||||
1. Open Brave and paste this address in the url bar: `brave://settings/privacy`
|
||||
2. Look for the option: "Use Google services for push messaging"
|
||||
3. Activate this option
|
||||
4. Relaunch Brave completely
|
||||
5. You should now see the notifications prompt appearing instead of an error message.
|
||||
|
||||
If you still encounter issues, please reach out on our support channels.
|
||||
|
||||
@@ -22,6 +22,17 @@ This is typically not needed. Please refer to your webhook provider's documentat
|
||||
|
||||
This value will be sent as an `Authorization` HTTP header.
|
||||
|
||||
### Custom Headers (optional)
|
||||
|
||||
You can add additional custom HTTP headers to be sent with each webhook request. This is useful for API keys, custom authentication schemes, or any other headers your webhook endpoint requires.
|
||||
|
||||
- Click "Add Header" to add a new header
|
||||
- Enter the header name and value
|
||||
|
||||
:::warning
|
||||
You cannot configure both the **Authorization Header** field and a custom `Authorization` header in Custom Headers at the same time. You must choose one method.
|
||||
:::
|
||||
|
||||
### JSON Payload
|
||||
|
||||
Customize the JSON payload to suit your needs. Seerr provides several [template variables](#template-variables) for use in the payload, which will be replaced with the relevant data when the notifications are triggered.
|
||||
|
||||
@@ -112,6 +112,10 @@ export interface JellyfinLibraryItemExtended extends JellyfinLibraryItem {
|
||||
DateCreated?: string;
|
||||
}
|
||||
|
||||
type EpisodeReturn<T> = T extends { includeMediaInfo: true }
|
||||
? JellyfinLibraryItemExtended[]
|
||||
: JellyfinLibraryItem[];
|
||||
|
||||
export interface JellyfinItemsReponse {
|
||||
Items: JellyfinLibraryItemExtended[];
|
||||
TotalRecordCount: number;
|
||||
@@ -415,13 +419,22 @@ class JellyfinAPI extends ExternalAPI {
|
||||
}
|
||||
}
|
||||
|
||||
public async getEpisodes(
|
||||
public async getEpisodes<
|
||||
T extends { includeMediaInfo?: boolean } | undefined = undefined
|
||||
>(
|
||||
seriesID: string,
|
||||
seasonID: string
|
||||
): Promise<JellyfinLibraryItem[]> {
|
||||
seasonID: string,
|
||||
options?: T
|
||||
): Promise<EpisodeReturn<T>> {
|
||||
try {
|
||||
const episodeResponse = await this.get<any>(
|
||||
`/Shows/${seriesID}/Episodes?seasonId=${seasonID}`
|
||||
`/Shows/${seriesID}/Episodes`,
|
||||
{
|
||||
params: {
|
||||
seasonId: seasonID,
|
||||
...(options?.includeMediaInfo && { fields: 'MediaSources' }),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return episodeResponse.Items.filter(
|
||||
|
||||
@@ -196,16 +196,33 @@ class WebhookAgent
|
||||
}
|
||||
|
||||
try {
|
||||
const headers: Record<string, string> = {};
|
||||
|
||||
if (settings.options.authHeader) {
|
||||
headers.Authorization = settings.options.authHeader;
|
||||
}
|
||||
|
||||
if (
|
||||
settings.options.customHeaders &&
|
||||
settings.options.customHeaders.length > 0
|
||||
) {
|
||||
settings.options.customHeaders.forEach((header) => {
|
||||
if (header.key && header.value) {
|
||||
// Don't override Authorization header if it's already set via authHeader
|
||||
if (
|
||||
header.key.toLowerCase() !== 'authorization' ||
|
||||
!settings.options.authHeader
|
||||
) {
|
||||
headers[header.key] = header.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await axios.post(
|
||||
webhookUrl,
|
||||
this.buildPayload(type, payload),
|
||||
settings.options.authHeader
|
||||
? {
|
||||
headers: {
|
||||
Authorization: settings.options.authHeader,
|
||||
},
|
||||
}
|
||||
: undefined
|
||||
Object.keys(headers).length > 0 ? { headers } : undefined
|
||||
);
|
||||
|
||||
return true;
|
||||
|
||||
@@ -374,9 +374,10 @@ class JellyfinScanner {
|
||||
) ?? []
|
||||
).length;
|
||||
|
||||
const jellyfinSeasons = await this.jfClient.getSeasons(Id);
|
||||
|
||||
for (const season of seasons) {
|
||||
const JellyfinSeasons = await this.jfClient.getSeasons(Id);
|
||||
const matchedJellyfinSeason = JellyfinSeasons.find((md) => {
|
||||
const matchedJellyfinSeason = jellyfinSeasons.find((md) => {
|
||||
if (tvdbSeasonFromAnidb) {
|
||||
// In AniDB we don't have the concept of seasons,
|
||||
// we have multiple shows with only Season 1 (and sometimes a season with index 0 for specials).
|
||||
@@ -397,38 +398,52 @@ class JellyfinScanner {
|
||||
|
||||
// Check if we found the matching season and it has all the available episodes
|
||||
if (matchedJellyfinSeason) {
|
||||
// If we have a matched Jellyfin season, get its children metadata so we can check details
|
||||
const episodes = await this.jfClient.getEpisodes(
|
||||
Id,
|
||||
matchedJellyfinSeason.Id
|
||||
);
|
||||
|
||||
//Get count of episodes that are HD and 4K
|
||||
let totalStandard = 0;
|
||||
let total4k = 0;
|
||||
|
||||
//use for loop to make sure this loop _completes_ in full
|
||||
//before the next section
|
||||
for (const episode of episodes) {
|
||||
let episodeCount = 1;
|
||||
if (!this.enable4kShow) {
|
||||
const episodes = await this.jfClient.getEpisodes(
|
||||
Id,
|
||||
matchedJellyfinSeason.Id
|
||||
);
|
||||
|
||||
// count number of combined episodes
|
||||
if (
|
||||
episode.IndexNumber !== undefined &&
|
||||
episode.IndexNumberEnd !== undefined
|
||||
) {
|
||||
episodeCount =
|
||||
episode.IndexNumberEnd - episode.IndexNumber + 1;
|
||||
}
|
||||
for (const episode of episodes) {
|
||||
let episodeCount = 1;
|
||||
|
||||
// count number of combined episodes
|
||||
if (
|
||||
episode.IndexNumber !== undefined &&
|
||||
episode.IndexNumberEnd !== undefined
|
||||
) {
|
||||
episodeCount =
|
||||
episode.IndexNumberEnd - episode.IndexNumber + 1;
|
||||
}
|
||||
|
||||
if (!this.enable4kShow) {
|
||||
totalStandard += episodeCount;
|
||||
} else {
|
||||
const ExtendedEpisodeData = await this.jfClient.getItemData(
|
||||
episode.Id
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 4K detection enabled - request media info to check resolution
|
||||
const episodes = await this.jfClient.getEpisodes(
|
||||
Id,
|
||||
matchedJellyfinSeason.Id,
|
||||
{ includeMediaInfo: true }
|
||||
);
|
||||
|
||||
ExtendedEpisodeData?.MediaSources?.some((MediaSource) => {
|
||||
for (const episode of episodes) {
|
||||
let episodeCount = 1;
|
||||
|
||||
// count number of combined episodes
|
||||
if (
|
||||
episode.IndexNumber !== undefined &&
|
||||
episode.IndexNumberEnd !== undefined
|
||||
) {
|
||||
episodeCount =
|
||||
episode.IndexNumberEnd - episode.IndexNumber + 1;
|
||||
}
|
||||
|
||||
// MediaSources field is included in response when includeMediaInfo is true
|
||||
// We iterate all MediaSources to detect if episode has both standard AND 4K versions
|
||||
episode.MediaSources?.some((MediaSource) => {
|
||||
return MediaSource.MediaStreams.some((MediaStream) => {
|
||||
if (MediaStream.Type === 'Video') {
|
||||
if ((MediaStream.Width ?? 0) >= 2000) {
|
||||
|
||||
@@ -275,6 +275,7 @@ export interface NotificationAgentWebhook extends NotificationAgentConfig {
|
||||
webhookUrl: string;
|
||||
jsonPayload: string;
|
||||
authHeader?: string;
|
||||
customHeaders?: { key: string; value: string }[];
|
||||
supportVariables?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -626,76 +626,6 @@ authRoutes.post('/local', async (req, res, next) => {
|
||||
});
|
||||
}
|
||||
|
||||
const mainUser = await userRepository.findOneOrFail({
|
||||
select: { id: true, plexToken: true, plexId: true },
|
||||
where: { id: 1 },
|
||||
});
|
||||
const mainPlexTv = new PlexTvAPI(mainUser.plexToken ?? '');
|
||||
|
||||
if (!user.plexId) {
|
||||
try {
|
||||
const plexUsersResponse = await mainPlexTv.getUsers();
|
||||
const account = plexUsersResponse.MediaContainer.User.find(
|
||||
(account) =>
|
||||
account.$.email &&
|
||||
account.$.email.toLowerCase() === user.email.toLowerCase()
|
||||
)?.$;
|
||||
|
||||
if (
|
||||
account &&
|
||||
(await mainPlexTv.checkUserAccess(parseInt(account.id)))
|
||||
) {
|
||||
logger.info(
|
||||
'Found matching Plex user; updating user with Plex data',
|
||||
{
|
||||
label: 'API',
|
||||
ip: req.ip,
|
||||
email: body.email,
|
||||
userId: user.id,
|
||||
plexId: account.id,
|
||||
plexUsername: account.username,
|
||||
}
|
||||
);
|
||||
|
||||
user.plexId = parseInt(account.id);
|
||||
user.avatar = account.thumb;
|
||||
user.email = account.email;
|
||||
user.plexUsername = account.username;
|
||||
user.userType = UserType.PLEX;
|
||||
|
||||
await userRepository.save(user);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Something went wrong fetching Plex users', {
|
||||
label: 'API',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
user.plexId &&
|
||||
user.plexId !== mainUser.plexId &&
|
||||
!(await mainPlexTv.checkUserAccess(user.plexId))
|
||||
) {
|
||||
logger.warn(
|
||||
'Failed sign-in attempt from Plex user without access to the media server',
|
||||
{
|
||||
label: 'API',
|
||||
account: {
|
||||
ip: req.ip,
|
||||
email: body.email,
|
||||
userId: user.id,
|
||||
plexId: user.plexId,
|
||||
},
|
||||
}
|
||||
);
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Access denied.',
|
||||
});
|
||||
}
|
||||
|
||||
// Set logged in session
|
||||
if (user && req.session) {
|
||||
req.session.userId = user.id;
|
||||
@@ -775,7 +705,7 @@ authRoutes.post('/logout', async (req, res, next) => {
|
||||
});
|
||||
return next({ status: 500, message: 'Failed to destroy session.' });
|
||||
}
|
||||
logger.info('Successfully logged out user', {
|
||||
logger.debug('Successfully logged out user', {
|
||||
label: 'Auth',
|
||||
userId,
|
||||
});
|
||||
|
||||
@@ -279,6 +279,7 @@ notificationRoutes.get('/webhook', (_req, res) => {
|
||||
'utf8'
|
||||
)
|
||||
),
|
||||
customHeaders: webhookSettings.options.customHeaders ?? [],
|
||||
supportVariables: webhookSettings.options.supportVariables ?? false,
|
||||
},
|
||||
};
|
||||
@@ -301,6 +302,7 @@ notificationRoutes.post('/webhook', async (req, res, next) => {
|
||||
),
|
||||
webhookUrl: req.body.options.webhookUrl,
|
||||
authHeader: req.body.options.authHeader,
|
||||
customHeaders: req.body.options.customHeaders ?? [],
|
||||
supportVariables: req.body.options.supportVariables ?? false,
|
||||
},
|
||||
};
|
||||
@@ -333,6 +335,7 @@ notificationRoutes.post('/webhook/test', async (req, res, next) => {
|
||||
),
|
||||
webhookUrl: req.body.options.webhookUrl,
|
||||
authHeader: req.body.options.authHeader,
|
||||
customHeaders: req.body.options.customHeaders ?? [],
|
||||
supportVariables: req.body.options.supportVariables ?? false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -5,7 +5,12 @@ import SettingsBadge from '@app/components/Settings/SettingsBadge';
|
||||
import globalMessages from '@app/i18n/globalMessages';
|
||||
import defineMessages from '@app/utils/defineMessages';
|
||||
import { isValidURL } from '@app/utils/urlValidationHelper';
|
||||
import { ArrowDownOnSquareIcon, BeakerIcon } from '@heroicons/react/24/outline';
|
||||
import {
|
||||
ArrowDownOnSquareIcon,
|
||||
BeakerIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
import {
|
||||
ArrowPathIcon,
|
||||
QuestionMarkCircleIcon,
|
||||
@@ -80,6 +85,16 @@ const messages = defineMessages(
|
||||
supportVariablesTip:
|
||||
'Available variables are documented in the webhook template variables section',
|
||||
authheader: 'Authorization Header',
|
||||
customHeaders: 'Custom Headers',
|
||||
customHeadersTip:
|
||||
'Add custom HTTP headers to include with webhook requests',
|
||||
customHeadersAdd: 'Add Header',
|
||||
customHeadersRemove: 'Remove',
|
||||
customHeadersKey: 'Header Name',
|
||||
customHeadersValue: 'Header Value',
|
||||
customHeadersIncomplete: 'All headers must have both name and value',
|
||||
customHeadersAuthConflict:
|
||||
'Cannot use both Authorization Header and custom Authorization header. Please remove one.',
|
||||
validationJsonPayloadRequired: 'You must provide a valid JSON payload',
|
||||
webhooksettingssaved: 'Webhook notification settings saved successfully!',
|
||||
webhooksettingsfailed: 'Webhook notification settings failed to save.',
|
||||
@@ -125,6 +140,43 @@ const NotificationsWebhook = () => {
|
||||
|
||||
supportVariables: Yup.boolean(),
|
||||
|
||||
customHeaders: Yup.array()
|
||||
.of(
|
||||
Yup.object().shape({
|
||||
key: Yup.string(),
|
||||
value: Yup.string(),
|
||||
})
|
||||
)
|
||||
.test(
|
||||
'complete-headers',
|
||||
intl.formatMessage(messages.customHeadersIncomplete),
|
||||
function (headers) {
|
||||
if (!headers || headers.length === 0) return true;
|
||||
return headers.every(
|
||||
(header) =>
|
||||
(!header.key || !header.key.trim()) ===
|
||||
(!header.value || !header.value.trim())
|
||||
);
|
||||
}
|
||||
)
|
||||
.test(
|
||||
'auth-conflict',
|
||||
intl.formatMessage(messages.customHeadersAuthConflict),
|
||||
function (headers) {
|
||||
const { authHeader } = this.parent;
|
||||
if (!authHeader || !headers || headers.length === 0) return true;
|
||||
|
||||
const hasCustomAuthHeader = headers.some(
|
||||
(header) =>
|
||||
header.key &&
|
||||
header.value &&
|
||||
header.key.toLowerCase() === 'authorization'
|
||||
);
|
||||
|
||||
return !hasCustomAuthHeader;
|
||||
}
|
||||
),
|
||||
|
||||
jsonPayload: Yup.string()
|
||||
.when('enabled', {
|
||||
is: true,
|
||||
@@ -159,6 +211,7 @@ const NotificationsWebhook = () => {
|
||||
webhookUrl: data.options.webhookUrl,
|
||||
jsonPayload: data.options.jsonPayload,
|
||||
authHeader: data.options.authHeader,
|
||||
customHeaders: data.options.customHeaders ?? [],
|
||||
supportVariables: data.options.supportVariables ?? false,
|
||||
}}
|
||||
validationSchema={NotificationsWebhookSchema}
|
||||
@@ -171,6 +224,9 @@ const NotificationsWebhook = () => {
|
||||
webhookUrl: values.webhookUrl,
|
||||
jsonPayload: JSON.stringify(values.jsonPayload),
|
||||
authHeader: values.authHeader,
|
||||
customHeaders: values.customHeaders.filter(
|
||||
(h: { key: string; value: string }) => h.key && h.value
|
||||
),
|
||||
supportVariables: values.supportVariables,
|
||||
},
|
||||
});
|
||||
@@ -229,6 +285,9 @@ const NotificationsWebhook = () => {
|
||||
webhookUrl: values.webhookUrl,
|
||||
jsonPayload: JSON.stringify(values.jsonPayload),
|
||||
authHeader: values.authHeader,
|
||||
customHeaders: values.customHeaders.filter(
|
||||
(h: { key: string; value: string }) => h.key && h.value
|
||||
),
|
||||
supportVariables: values.supportVariables ?? false,
|
||||
},
|
||||
});
|
||||
@@ -344,6 +403,86 @@ const NotificationsWebhook = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="customHeaders" className="text-label">
|
||||
{intl.formatMessage(messages.customHeaders)}
|
||||
<span className="label-tip">
|
||||
{intl.formatMessage(messages.customHeadersTip)}
|
||||
</span>
|
||||
</label>
|
||||
<div className="form-input-area">
|
||||
<div className="space-y-2">
|
||||
{values.customHeaders.map(
|
||||
(header: { key: string; value: string }, index: number) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<div className="flex-1">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
name={`customHeaders.${index}.key`}
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.customHeadersKey
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="form-input-field">
|
||||
<Field
|
||||
name={`customHeaders.${index}.value`}
|
||||
type="text"
|
||||
placeholder={intl.formatMessage(
|
||||
messages.customHeadersValue
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
buttonType="danger"
|
||||
buttonSize="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const newHeaders = values.customHeaders.filter(
|
||||
(
|
||||
_: { key: string; value: string },
|
||||
i: number
|
||||
) => i !== index
|
||||
);
|
||||
setFieldValue('customHeaders', newHeaders);
|
||||
}}
|
||||
title={intl.formatMessage(
|
||||
messages.customHeadersRemove
|
||||
)}
|
||||
>
|
||||
<TrashIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
<Button
|
||||
buttonType="default"
|
||||
buttonSize="sm"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setFieldValue('customHeaders', [
|
||||
...values.customHeaders,
|
||||
{ key: '', value: '' },
|
||||
]);
|
||||
}}
|
||||
>
|
||||
<PlusIcon />
|
||||
<span>{intl.formatMessage(messages.customHeadersAdd)}</span>
|
||||
</Button>
|
||||
</div>
|
||||
{errors.customHeaders &&
|
||||
touched.customHeaders &&
|
||||
typeof errors.customHeaders === 'string' && (
|
||||
<div className="error">{errors.customHeaders}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label htmlFor="webhook-json-payload" className="text-label">
|
||||
{intl.formatMessage(messages.customJson)}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { UserType } from '@server/constants/user';
|
||||
import type { PermissionCheckOptions } from '@server/lib/permissions';
|
||||
import { hasPermission, Permission } from '@server/lib/permissions';
|
||||
import type { NotificationAgentKey } from '@server/lib/settings';
|
||||
import { useRouter } from 'next/router';
|
||||
import type { MutatorCallback } from 'swr';
|
||||
import useSWR from 'swr';
|
||||
|
||||
@@ -56,13 +57,21 @@ export const useUser = ({
|
||||
id,
|
||||
initialData,
|
||||
}: { id?: number; initialData?: User } = {}): UserHookResponse => {
|
||||
const router = useRouter();
|
||||
const isAuthPage = /^\/(login|setup|resetpassword(?:\/|$))/.test(
|
||||
router.pathname
|
||||
);
|
||||
|
||||
const {
|
||||
data,
|
||||
error,
|
||||
mutate: revalidate,
|
||||
} = useSWR<User>(id ? `/api/v1/user/${id}` : `/api/v1/auth/me`, {
|
||||
fallbackData: initialData,
|
||||
refreshInterval: 30000,
|
||||
refreshInterval: !isAuthPage ? 30000 : 0,
|
||||
revalidateOnFocus: !isAuthPage,
|
||||
revalidateOnMount: !isAuthPage,
|
||||
revalidateOnReconnect: !isAuthPage,
|
||||
errorRetryInterval: 30000,
|
||||
shouldRetryOnError: false,
|
||||
});
|
||||
|
||||
@@ -681,6 +681,14 @@
|
||||
"components.Settings.Notifications.NotificationsSlack.webhookUrlTip": "Create an <WebhookLink>Incoming Webhook</WebhookLink> integration",
|
||||
"components.Settings.Notifications.NotificationsWebhook.agentenabled": "Enable Agent",
|
||||
"components.Settings.Notifications.NotificationsWebhook.authheader": "Authorization Header",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customHeaders": "Custom Headers",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customHeadersAdd": "Add Header",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customHeadersAuthConflict": "Cannot use both Authorization Header and custom Authorization header. Please remove one.",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customHeadersIncomplete": "All headers must have both name and value",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customHeadersKey": "Header Name",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customHeadersRemove": "Remove",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customHeadersTip": "Add custom HTTP headers to include with webhook requests",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customHeadersValue": "Header Value",
|
||||
"components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload",
|
||||
"components.Settings.Notifications.NotificationsWebhook.resetPayload": "Reset to Default",
|
||||
"components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON payload reset successfully!",
|
||||
|
||||
Reference in New Issue
Block a user