mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(settings): logs viewer (#997)
This commit is contained in:

committed by
GitHub

parent
d76bf32c9d
commit
54429bbc1d
@@ -2252,6 +2252,54 @@ paths:
|
||||
responses:
|
||||
'204':
|
||||
description: 'Flushed cache'
|
||||
/settings/logs:
|
||||
get:
|
||||
summary: Returns logs
|
||||
description: Returns list of all log items and details
|
||||
tags:
|
||||
- settings
|
||||
parameters:
|
||||
- in: query
|
||||
name: take
|
||||
schema:
|
||||
type: number
|
||||
nullable: true
|
||||
example: 25
|
||||
- in: query
|
||||
name: skip
|
||||
schema:
|
||||
type: number
|
||||
nullable: true
|
||||
example: 0
|
||||
- in: query
|
||||
name: filter
|
||||
schema:
|
||||
type: string
|
||||
nullable: true
|
||||
enum: [debug, info, warn, error]
|
||||
default: debug
|
||||
responses:
|
||||
'200':
|
||||
description: Server log returned
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
label:
|
||||
type: string
|
||||
example: server
|
||||
level:
|
||||
type: string
|
||||
example: info
|
||||
message:
|
||||
type: string
|
||||
example: Server ready on port 5055
|
||||
timestamp:
|
||||
type: string
|
||||
example: 2020-12-15T16:20:00.069Z
|
||||
/settings/notifications:
|
||||
get:
|
||||
summary: Return notification settings
|
||||
|
@@ -33,6 +33,7 @@
|
||||
"email-templates": "^8.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-openapi-validator": "^4.12.5",
|
||||
"express-rate-limit": "^5.2.6",
|
||||
"express-session": "^1.17.1",
|
||||
"formik": "^2.2.6",
|
||||
"gravatar-url": "^3.1.0",
|
||||
@@ -89,6 +90,7 @@
|
||||
"@types/csurf": "^1.11.0",
|
||||
"@types/email-templates": "^8.0.2",
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/express-rate-limit": "^5.1.1",
|
||||
"@types/express-session": "^1.17.3",
|
||||
"@types/lodash": "^4.14.168",
|
||||
"@types/node": "^14.14.35",
|
||||
|
@@ -1,3 +1,16 @@
|
||||
import type { PaginatedResponse } from './common';
|
||||
|
||||
export type LogMessage = {
|
||||
timestamp: string;
|
||||
level: string;
|
||||
label: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface LogsResultsResponse extends PaginatedResponse {
|
||||
results: LogMessage[];
|
||||
}
|
||||
|
||||
export interface SettingsAboutResponse {
|
||||
version: string;
|
||||
totalRequests: number;
|
||||
|
@@ -1,22 +1,30 @@
|
||||
import { Router } from 'express';
|
||||
import { getSettings, Library, MainSettings } from '../../lib/settings';
|
||||
import rateLimit from 'express-rate-limit';
|
||||
import fs from 'fs';
|
||||
import { merge, omit } from 'lodash';
|
||||
import path from 'path';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { User } from '../../entity/User';
|
||||
import PlexAPI from '../../api/plexapi';
|
||||
import PlexTvAPI from '../../api/plextv';
|
||||
import { scheduledJobs } from '../../job/schedule';
|
||||
import { Permission } from '../../lib/permissions';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
import { merge, omit } from 'lodash';
|
||||
import Media from '../../entity/Media';
|
||||
import { MediaRequest } from '../../entity/MediaRequest';
|
||||
import { getAppVersion } from '../../utils/appVersion';
|
||||
import { SettingsAboutResponse } from '../../interfaces/api/settingsInterfaces';
|
||||
import notificationRoutes from './notifications';
|
||||
import sonarrRoutes from './sonarr';
|
||||
import radarrRoutes from './radarr';
|
||||
import { User } from '../../entity/User';
|
||||
import {
|
||||
LogMessage,
|
||||
LogsResultsResponse,
|
||||
SettingsAboutResponse,
|
||||
} from '../../interfaces/api/settingsInterfaces';
|
||||
import { scheduledJobs } from '../../job/schedule';
|
||||
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
|
||||
import { Permission } from '../../lib/permissions';
|
||||
import { plexFullScanner } from '../../lib/scanners/plex';
|
||||
import { getSettings, Library, MainSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
import { getAppVersion } from '../../utils/appVersion';
|
||||
import notificationRoutes from './notifications';
|
||||
import radarrRoutes from './radarr';
|
||||
import sonarrRoutes from './sonarr';
|
||||
|
||||
const settingsRoutes = Router();
|
||||
|
||||
@@ -223,6 +231,82 @@ settingsRoutes.post('/plex/sync', (req, res) => {
|
||||
return res.status(200).json(plexFullScanner.status());
|
||||
});
|
||||
|
||||
settingsRoutes.get(
|
||||
'/logs',
|
||||
rateLimit({ windowMs: 60 * 1000, max: 50 }),
|
||||
(req, res, next) => {
|
||||
const pageSize = req.query.take ? Number(req.query.take) : 25;
|
||||
const skip = req.query.skip ? Number(req.query.skip) : 0;
|
||||
|
||||
let filter: string[] = [];
|
||||
switch (req.query.filter) {
|
||||
case 'debug':
|
||||
filter.push('debug');
|
||||
// falls through
|
||||
case 'info':
|
||||
filter.push('info');
|
||||
// falls through
|
||||
case 'warn':
|
||||
filter.push('warn');
|
||||
// falls through
|
||||
case 'error':
|
||||
filter.push('error');
|
||||
break;
|
||||
default:
|
||||
filter = ['debug', 'info', 'warn', 'error'];
|
||||
}
|
||||
|
||||
const logFile = process.env.CONFIG_DIRECTORY
|
||||
? `${process.env.CONFIG_DIRECTORY}/logs/overseerr.log`
|
||||
: path.join(__dirname, '../../../config/logs/overseerr.log');
|
||||
const logs: LogMessage[] = [];
|
||||
|
||||
try {
|
||||
fs.readFileSync(logFile)
|
||||
.toString()
|
||||
.split('\n')
|
||||
.forEach((line) => {
|
||||
if (!line.length) return;
|
||||
|
||||
const timestamp = line.match(new RegExp(/^.{24}/)) || [];
|
||||
const level = line.match(new RegExp(/\s\[\w+\]/)) || [];
|
||||
const label = line.match(new RegExp(/[^\s]\[\w+\s*\w*\]/)) || [];
|
||||
const message = line.match(new RegExp(/:\s.*/)) || [];
|
||||
|
||||
if (level.length && filter.includes(level[0].slice(2, -1))) {
|
||||
logs.push({
|
||||
timestamp: timestamp[0],
|
||||
level: level.length ? level[0].slice(2, -1) : '',
|
||||
label: label.length ? label[0].slice(2, -1) : '',
|
||||
message: message.length ? message[0].slice(2) : '',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const displayedLogs = logs.reverse().slice(skip, skip + pageSize);
|
||||
|
||||
return res.status(200).json({
|
||||
pageInfo: {
|
||||
pages: Math.ceil(logs.length / pageSize),
|
||||
pageSize,
|
||||
results: logs.length,
|
||||
page: Math.ceil(skip / pageSize) + 1,
|
||||
},
|
||||
results: displayedLogs,
|
||||
} as LogsResultsResponse);
|
||||
} catch (error) {
|
||||
logger.error('Something went wrong while fetching the logs', {
|
||||
label: 'Logs',
|
||||
errorMessage: error.message,
|
||||
});
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'Something went wrong while fetching the logs',
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
settingsRoutes.get('/jobs', (_req, res) => {
|
||||
return res.status(200).json(
|
||||
scheduledJobs.map((job) => ({
|
||||
|
@@ -59,7 +59,7 @@ const RequestList: React.FC = () => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Set fitler values to local storage any time they are changed
|
||||
// Set filter values to local storage any time they are changed
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(
|
||||
'rl-filter-settings',
|
||||
|
@@ -1,14 +1,292 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import useSWR from 'swr';
|
||||
import {
|
||||
LogMessage,
|
||||
LogsResultsResponse,
|
||||
} from '../../../../server/interfaces/api/settingsInterfaces';
|
||||
import Error from '../../../pages/_error';
|
||||
import Badge from '../../Common/Badge';
|
||||
import Button from '../../Common/Button';
|
||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||
import Table from '../../Common/Table';
|
||||
|
||||
// We will localize this file when the complete version is released.
|
||||
const messages = defineMessages({
|
||||
logs: 'Logs',
|
||||
logsDescription:
|
||||
'You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/overseerr.log</code>.',
|
||||
time: 'Timestamp',
|
||||
level: 'Severity',
|
||||
label: 'Label',
|
||||
message: 'Message',
|
||||
filterDebug: 'Debug',
|
||||
filterInfo: 'Info',
|
||||
filterWarn: 'Warning',
|
||||
filterError: 'Error',
|
||||
noresults: 'No results.',
|
||||
showall: 'Show All Logs',
|
||||
showingresults:
|
||||
'Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results',
|
||||
resultsperpage: 'Display {pageSize} results per page',
|
||||
next: 'Next',
|
||||
previous: 'Previous',
|
||||
pauseLogs: 'Pause',
|
||||
resumeLogs: 'Resume',
|
||||
});
|
||||
|
||||
type Filter = 'debug' | 'info' | 'warn' | 'error';
|
||||
|
||||
const SettingsLogs: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [currentFilter, setCurrentFilter] = useState<Filter>('debug');
|
||||
const [currentPageSize, setCurrentPageSize] = useState(25);
|
||||
const [refreshInterval, setRefreshInterval] = useState(5000);
|
||||
|
||||
const toggleLogs = () => {
|
||||
setRefreshInterval(refreshInterval === 5000 ? 0 : 5000);
|
||||
};
|
||||
|
||||
const { data, error } = useSWR<LogsResultsResponse>(
|
||||
`/api/v1/settings/logs?take=${currentPageSize}&skip=${
|
||||
pageIndex * currentPageSize
|
||||
}&filter=${currentFilter}`,
|
||||
{
|
||||
refreshInterval: refreshInterval,
|
||||
revalidateOnFocus: false,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: appData } = useSWR('/api/v1/status/appdata');
|
||||
|
||||
useEffect(() => {
|
||||
const displayString = window.localStorage.getItem('logs-display-settings');
|
||||
|
||||
if (displayString) {
|
||||
const displaySettings = JSON.parse(displayString);
|
||||
|
||||
setCurrentFilter(displaySettings.currentFilter);
|
||||
setCurrentPageSize(displaySettings.currentPageSize);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.localStorage.setItem(
|
||||
'logs-display-settings',
|
||||
JSON.stringify({
|
||||
currentFilter,
|
||||
currentPageSize,
|
||||
})
|
||||
);
|
||||
}, [currentFilter, currentPageSize]);
|
||||
|
||||
if (!data && !error) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return <Error statusCode={500} />;
|
||||
}
|
||||
|
||||
const hasNextPage = data.pageInfo.pages > pageIndex + 1;
|
||||
const hasPrevPage = pageIndex > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="text-sm leading-loose text-gray-300">
|
||||
This page is still being built. For now, you can access your logs
|
||||
directly in <code>stdout</code> (container logs) or looking in{' '}
|
||||
<code>/app/config/logs/overseerr.log</code>.
|
||||
<div className="mb-2">
|
||||
<h3 className="heading">{intl.formatMessage(messages.logs)}</h3>
|
||||
<p className="description">
|
||||
{intl.formatMessage(messages.logsDescription, {
|
||||
code: function code(msg) {
|
||||
return <code className="bg-opacity-50">{msg}</code>;
|
||||
},
|
||||
configDir: appData ? appData.appDataPath : '/app/config',
|
||||
})}
|
||||
</p>
|
||||
<div className="flex flex-row flex-grow mt-2 sm:flex-grow-0 sm:justify-end">
|
||||
<div className="flex flex-row justify-between flex-1 mb-2 sm:mb-0 sm:flex-none">
|
||||
<Button
|
||||
className="flex-grow w-full mr-2 sm:w-24"
|
||||
buttonType={refreshInterval ? 'default' : 'primary'}
|
||||
onClick={() => toggleLogs()}
|
||||
>
|
||||
{intl.formatMessage(
|
||||
refreshInterval ? messages.pauseLogs : messages.resumeLogs
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-1 mb-2 sm:mb-0 sm:flex-none">
|
||||
<span className="inline-flex items-center px-3 text-sm text-gray-100 bg-gray-800 border border-r-0 border-gray-500 cursor-default rounded-l-md">
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 3a1 1 0 011-1h12a1 1 0 011 1v3a1 1 0 01-.293.707L12 11.414V15a1 1 0 01-.293.707l-2 2A1 1 0 018 17v-5.586L3.293 6.707A1 1 0 013 6V3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<select
|
||||
id="filter"
|
||||
name="filter"
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentFilter(e.target.value as Filter);
|
||||
}}
|
||||
value={currentFilter}
|
||||
className="rounded-r-only"
|
||||
>
|
||||
<option value="debug">
|
||||
{intl.formatMessage(messages.filterDebug)}
|
||||
</option>
|
||||
<option value="info">
|
||||
{intl.formatMessage(messages.filterInfo)}
|
||||
</option>
|
||||
<option value="warn">
|
||||
{intl.formatMessage(messages.filterWarn)}
|
||||
</option>
|
||||
<option value="error">
|
||||
{intl.formatMessage(messages.filterError)}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<Table.TH>{intl.formatMessage(messages.time)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.level)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.label)}</Table.TH>
|
||||
<Table.TH>{intl.formatMessage(messages.message)}</Table.TH>
|
||||
</tr>
|
||||
</thead>
|
||||
<Table.TBody>
|
||||
{data.results.map((row: LogMessage, index: number) => {
|
||||
return (
|
||||
<tr key={`log-list-${index}`}>
|
||||
<Table.TD className="text-gray-300">
|
||||
{intl.formatDate(row.timestamp, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: false,
|
||||
})}
|
||||
</Table.TD>
|
||||
<Table.TD className="text-gray-300">
|
||||
<Badge
|
||||
badgeType={
|
||||
row.level === 'error'
|
||||
? 'danger'
|
||||
: row.level === 'warn'
|
||||
? 'warning'
|
||||
: row.level === 'info'
|
||||
? 'success'
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{row.level.toUpperCase()}
|
||||
</Badge>
|
||||
</Table.TD>
|
||||
<Table.TD className="text-gray-300">{row.label}</Table.TD>
|
||||
<Table.TD className="text-gray-300">{row.message}</Table.TD>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
{data.results.length === 0 && (
|
||||
<tr className="relative h-24 p-2 text-white">
|
||||
<Table.TD colSpan={4} noPadding>
|
||||
<div className="flex flex-col items-center justify-center w-screen p-6 lg:w-full">
|
||||
<span className="text-base">
|
||||
{intl.formatMessage(messages.noresults)}
|
||||
</span>
|
||||
{currentFilter !== 'debug' && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
buttonSize="sm"
|
||||
buttonType="primary"
|
||||
onClick={() => setCurrentFilter('debug')}
|
||||
>
|
||||
{intl.formatMessage(messages.showall)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Table.TD>
|
||||
</tr>
|
||||
)}
|
||||
<tr className="bg-gray-700">
|
||||
<Table.TD colSpan={6} noPadding>
|
||||
<nav
|
||||
className="flex flex-col items-center w-screen px-6 py-3 space-x-4 space-y-3 sm:space-y-0 sm:flex-row lg:w-full"
|
||||
aria-label="Pagination"
|
||||
>
|
||||
<div className="hidden lg:flex lg:flex-1">
|
||||
<p className="text-sm">
|
||||
{data.results.length > 0 &&
|
||||
intl.formatMessage(messages.showingresults, {
|
||||
from: pageIndex * currentPageSize + 1,
|
||||
to:
|
||||
data.results.length < currentPageSize
|
||||
? pageIndex * currentPageSize +
|
||||
data.results.length
|
||||
: (pageIndex + 1) * currentPageSize,
|
||||
total: data.pageInfo.results,
|
||||
strong: function strong(msg) {
|
||||
return <span className="font-medium">{msg}</span>;
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-center sm:flex-1 sm:justify-start lg:justify-center">
|
||||
<span className="items-center -mt-3 text-sm sm:-ml-4 lg:ml-0 sm:mt-0">
|
||||
{intl.formatMessage(messages.resultsperpage, {
|
||||
pageSize: (
|
||||
<select
|
||||
id="pageSize"
|
||||
name="pageSize"
|
||||
onChange={(e) => {
|
||||
setPageIndex(0);
|
||||
setCurrentPageSize(Number(e.target.value));
|
||||
}}
|
||||
value={currentPageSize}
|
||||
className="inline short"
|
||||
>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-center flex-auto space-x-2 sm:justify-end sm:flex-1">
|
||||
<Button
|
||||
disabled={!hasPrevPage}
|
||||
onClick={() => setPageIndex((current) => current - 1)}
|
||||
>
|
||||
{intl.formatMessage(messages.previous)}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!hasNextPage}
|
||||
onClick={() => setPageIndex((current) => current + 1)}
|
||||
>
|
||||
{intl.formatMessage(messages.next)}
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
</Table.TD>
|
||||
</tr>
|
||||
</Table.TBody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
@@ -453,6 +453,24 @@
|
||||
"components.Settings.SettingsJobsCache.runnow": "Run Now",
|
||||
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan",
|
||||
"components.Settings.SettingsJobsCache.unknownJob": "Unknown Job",
|
||||
"components.Settings.SettingsLogs.filterDebug": "Debug",
|
||||
"components.Settings.SettingsLogs.filterError": "Error",
|
||||
"components.Settings.SettingsLogs.filterInfo": "Info",
|
||||
"components.Settings.SettingsLogs.filterWarn": "Warning",
|
||||
"components.Settings.SettingsLogs.label": "Label",
|
||||
"components.Settings.SettingsLogs.level": "Severity",
|
||||
"components.Settings.SettingsLogs.logs": "Logs",
|
||||
"components.Settings.SettingsLogs.logsDescription": "You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/overseerr.log</code>.",
|
||||
"components.Settings.SettingsLogs.message": "Message",
|
||||
"components.Settings.SettingsLogs.next": "Next",
|
||||
"components.Settings.SettingsLogs.noresults": "No results.",
|
||||
"components.Settings.SettingsLogs.pauseLogs": "Pause",
|
||||
"components.Settings.SettingsLogs.previous": "Previous",
|
||||
"components.Settings.SettingsLogs.resultsperpage": "Display {pageSize} results per page",
|
||||
"components.Settings.SettingsLogs.resumeLogs": "Resume",
|
||||
"components.Settings.SettingsLogs.showall": "Show All Logs",
|
||||
"components.Settings.SettingsLogs.showingresults": "Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results",
|
||||
"components.Settings.SettingsLogs.time": "Timestamp",
|
||||
"components.Settings.SettingsUsers.defaultPermissions": "Default User Permissions",
|
||||
"components.Settings.SettingsUsers.localLogin": "Enable Local User Sign-In",
|
||||
"components.Settings.SettingsUsers.save": "Save Changes",
|
||||
|
12
yarn.lock
12
yarn.lock
@@ -2097,6 +2097,13 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884"
|
||||
integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==
|
||||
|
||||
"@types/express-rate-limit@^5.1.1":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-rate-limit/-/express-rate-limit-5.1.1.tgz#e5b0239d18c1580e52ae56dce4248333302a1dc8"
|
||||
integrity sha512-6oMYZBLlhxC5sdcRXXz528QyfGz3zTy9YdHwqlxLfgx5Cd3zwYaUjjPpJcaTtHmRefLi9P8kLBPz2wB7yz4JtQ==
|
||||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
"@types/express-serve-static-core@*":
|
||||
version "4.17.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.9.tgz#2d7b34dcfd25ec663c25c85d76608f8b249667f1"
|
||||
@@ -5714,6 +5721,11 @@ express-openapi-validator@^4.12.5:
|
||||
ono "^7.1.3"
|
||||
path-to-regexp "^6.2.0"
|
||||
|
||||
express-rate-limit@^5.2.6:
|
||||
version "5.2.6"
|
||||
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0"
|
||||
integrity sha512-nE96xaxGfxiS5jP3tD3kIW1Jg9yQgX0rXCs3rCkZtmbWHEGyotwaezkLj7bnB41Z0uaOLM8W4AX6qHao4IZ2YA==
|
||||
|
||||
express-session@^1.15.6, express-session@^1.17.1:
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.1.tgz#36ecbc7034566d38c8509885c044d461c11bf357"
|
||||
|
Reference in New Issue
Block a user