diff --git a/.github/workflows/create-tag.yml b/.github/workflows/create-tag.yml index e532d6be1..97975b069 100644 --- a/.github/workflows/create-tag.yml +++ b/.github/workflows/create-tag.yml @@ -78,7 +78,7 @@ jobs: - name: Commit updated files run: | git add package.json - git commit -m 'chore(release): prepare ${TAG_VERSION}' + git commit -m "chore(release): prepare ${TAG_VERSION}" git push - name: Create git tag diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 733eefb76..1d1581c7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -75,7 +75,7 @@ Please be respectful to maintainers and disclose AI assistance. 3. Create a new branch: ```bash - git checkout -b BRANCH_NAME develop + git switch -c BRANCH_NAME develop ``` - It is recommended to give your branch a meaningful name, relevant to the feature or fix you are working on. @@ -127,11 +127,10 @@ Steps: ### Contributing Code - If you are taking on an existing bug or feature ticket, please comment on the [issue](/../../issues) to avoid multiple people working on the same thing. -- All commits **must** follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) - - Pull requests with titles or commits not following this standard will **not** be merged. PR titles are automatically checked for compliance. +- Pull requests with titles not following [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) will **not** be merged. PR titles are automatically checked for compliance. - Please make meaningful commits, or squash them prior to opening a pull request. - Do not squash commits once people have begun reviewing your changes. -- Always rebase your commit to the latest `develop` branch. Do **not** merge `develop` into your branch. +- Always rebase your branch to the latest `develop` branch. - It is your responsibility to keep your branch up-to-date. Your work will **not** be merged unless it is rebased off the latest `develop` branch. - You can create a "draft" pull request early to get feedback on your work. - Your code **must** be formatted correctly, or the tests will fail. @@ -180,10 +179,10 @@ PGPASSWORD=postgres sudo docker exec -it postgres-seerr /usr/bin/psql -h 127.0.0 PGPASSWORD=postgres sudo docker exec -it postgres-seerr /usr/bin/psql -h 127.0.0.1 -U postgres -c "CREATE DATABASE seerr;" ``` -3. Checkout the `develop` branch and create the original database for SQLite and PostgreSQL so that TypeORM can automatically generate the migrations: +3. Switch to the `develop` branch and create the original database for SQLite and PostgreSQL so that TypeORM can automatically generate the migrations: ```bash -git checkout develop +git switch develop pnpm i rm -r .next dist; pnpm build pnpm start @@ -195,7 +194,7 @@ DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm start 4. Let TypeORM generate the migrations: ```bash -git checkout -b your-feature-branch +git switch -c your-feature-branch pnpm i pnpm migration:generate server/migration/sqlite/YourMigrationName DB_TYPE="postgres" DB_USER=postgres DB_PASS=postgres pnpm migration:generate server/migration/postgres/YourMigrationName diff --git a/SECURITY.md b/SECURITY.md index f50d56110..5f20209c3 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -8,6 +8,51 @@ To report a security issue, please use the GitHub Security Advisory ["Report a V **Please do not report security vulnerabilities through public GitHub issues, discussions, or Discord.** +## AI Assistance Notice + +> [!IMPORTANT] +> +> Automated AI-generated contributions without human review are not allowed and will be rejected. +> This is an open-source project maintained by volunteers. +> We do not have the resources to review pull requests that could have been avoided with proper human oversight. +> While we have no issue with contributors using AI tools as an aid, it is your responsibility as a contributor to ensure that all submissions are carefully reviewed and meet our quality standards. +> Submissions that appear to be unreviewed AI output will be considered low-effort and may result in a ban. +> +> If you are using **any kind of AI assistance** to contribute to Seerr, +> it must be disclosed in the pull request. + +If you are using any kind of AI assistance while contributing to Seerr, +**this must be disclosed in the pull request**, along with the extent to +which AI assistance was used (e.g. docs only vs. code generation). +If security advisory responses are being generated by an AI, disclose that as well. +As a small exception, trivial tab-completion doesn't need to be disclosed, +so long as it is limited to single keywords or short phrases. + +An example disclosure: + +> This security advisory was written primarily by Claude Code. + +Or a more detailed disclosure: + +> I consulted ChatGPT to understand the codebase but the solution +> was fully authored manually by myself. + +Failure to disclose this is first and foremost rude to the human operators +on the other end of the pull request, but it also makes it difficult to +determine how much scrutiny to apply to the contribution. + +In a perfect world, AI assistance would produce equal or higher quality +work than any human. That isn't the world we live in today, and in most cases +it's generating slop. I say this despite being a fan of and using them +successfully myself (with heavy supervision)! + +When using AI assistance, we expect contributors to understand the code +that is produced and be able to answer critical questions about it. It +isn't a maintainers job to review a PR so broken that it requires +significant rework to be acceptable. + +Please be respectful to maintainers and disclose AI assistance. + ## What to Include in Your Report To help us better understand and resolve the issue, please include as much of the following information as possible: diff --git a/charts/seerr-chart/Chart.yaml b/charts/seerr-chart/Chart.yaml index 2b531ae12..3039937a1 100644 --- a/charts/seerr-chart/Chart.yaml +++ b/charts/seerr-chart/Chart.yaml @@ -3,9 +3,9 @@ kubeVersion: '>=1.23.0-0' name: seerr-chart description: Seerr helm chart for Kubernetes type: application -version: 3.2.0 +version: 3.3.0 # renovate: image=ghcr.io/seerr-team/seerr -appVersion: 'v3.0.1' +appVersion: 'v3.1.0' maintainers: - name: Seerr Team url: https://github.com/orgs/seerr-team/people diff --git a/charts/seerr-chart/README.md b/charts/seerr-chart/README.md index 9b0546ee4..4fdcde991 100644 --- a/charts/seerr-chart/README.md +++ b/charts/seerr-chart/README.md @@ -1,6 +1,6 @@ # seerr-chart -![Version: 3.2.0](https://img.shields.io/badge/Version-3.2.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.0.1](https://img.shields.io/badge/AppVersion-v3.0.1-informational?style=flat-square) +![Version: 3.3.0](https://img.shields.io/badge/Version-3.3.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square) ![AppVersion: v3.1.0](https://img.shields.io/badge/AppVersion-v3.1.0-informational?style=flat-square) Seerr helm chart for Kubernetes diff --git a/docs/using-seerr/notifications/ntfy.md b/docs/using-seerr/notifications/ntfy.md index 946dca781..72a3790f5 100644 --- a/docs/using-seerr/notifications/ntfy.md +++ b/docs/using-seerr/notifications/ntfy.md @@ -24,6 +24,10 @@ Set this to the username and password for your ntfy.sh server. Set this to the token for your ntfy.sh server. +### Priority (optional) + +Set the priority level for notifications. Options range from Minimum (1) to Urgent (5), with Default (3) being the standard level. Higher priority notifications may bypass Do Not Disturb settings on some devices. + :::info Please refer to the [ntfy.sh API documentation](https://docs.ntfy.sh/) for more details on configuring these notifications. ::: diff --git a/gen-docs/blog/2026-02-28-seerr-security-fix-release.md b/gen-docs/blog/2026-02-28-seerr-security-fix-release.md new file mode 100644 index 000000000..bfe5077ad --- /dev/null +++ b/gen-docs/blog/2026-02-28-seerr-security-fix-release.md @@ -0,0 +1,72 @@ +--- +title: "Seerr v3.1.0: Critical Security Release" +description: "Seerr v3.1.0 addresses three CVEs, including a high-priority vulnerability affecting Plex-configured instances. Upgrade immediately." +slug: seerr-3-1-0-security-release +authors: [seerr-team] +image: https://raw.githubusercontent.com/seerr-team/seerr/refs/heads/develop/gen-docs/static/img/logo_full.svg +hide_table_of_contents: false +--- + +We are releasing **Seerr v3.1.0**, a security-focused update that addresses three CVEs, including a high-priority vulnerability affecting instances configured with Plex Media Server. **We strongly recommend upgrading as soon as possible.** + +This release also includes a number of bug fixes and marks the end of our post-merger feature freeze. New features will be resuming in future updates. + + + +## Security Vulnerabilities + +This release patches three newly identified CVEs. If you are running a Plex-configured instance of Seerr, **one of these vulnerabilities is high priority and poses a significant risk**, please upgrade immediately. + +### [CVE-2026-27707](https://github.com/seerr-team/seerr/security/advisories/GHSA-rc4w-7m3r-c2f7) — Unauthenticated Account Registration via Jellyfin Endpoint (High) + +On instances configured to use Plex as the media server, an unauthenticated attacker could register an account by abusing the Jellyfin authentication endpoint. This could allow unauthorized users to gain access to your Seerr instance without valid Plex credentials. + +### [CVE-2026-27793](https://github.com/seerr-team/seerr/security/advisories/GHSA-f7xw-jcqr-57hp) — Broken Object-Level Authorization in User Profile Endpoint (Medium) + +A broken object-level authorization vulnerability in the user profile endpoint could allow an authenticated user to access another user's profile data, including third-party notification credentials such as webhook URLs, Telegram tokens, and similar sensitive configuration. + +### [CVE-2026-27792](https://github.com/seerr-team/seerr/security/advisories/GHSA-gx3h-3jg5-q65f) — Missing Authentication on Push Subscription Endpoints (Medium) + +The push subscription endpoints lacked proper authentication checks, allowing unauthenticated requests to interact with subscription management functionality. + +--- + +Please review the full security advisories linked above for technical details, impact assessment, and mitigation steps. + +## Bug Fixes + +Alongside the security patches, this release ships a number of bug fixes: + +- ***(helm)*** Add `"v"` as prefix for `appVersion` tag +- ***(jellyfin-scanner)*** Include unmatched seasons in processable seasons +- ***(link-account)*** Fix error-message override +- ***(plex-scanner)*** Add TVDb to TMDB fallback in Plex scanner +- ***(radarr)*** Trigger search for existing monitored movies without files +- ***(servarr)*** Increase default API timeout from 5000ms to 10000ms +- ***(sonarr)*** Use configured metadata provider for season filtering +- ***(watch-data)*** Use sentinel values to avoid invalid SQL syntax +- ***(watchlist-sync)*** Correct permission typo for TV auto requests +- Preserve blocklist on media deletion & optimise watchlist-sync + +## New Contributors + +Many thanks to those making their first contribution to Seerr in this release: + +* [@caillou](https://github.com/caillou) +* [@Kenshin9977](https://github.com/Kenshin9977) +* [@MagicLegend](https://github.com/MagicLegend) +* [@wiiaam](https://github.com/wiiaam) +* [@mjonkus](https://github.com/mjonkus) +* [@nova-api](https://github.com/nova-api) +* [@mreid-tt](https://github.com/mreid-tt) +* [@DataBitz](https://github.com/DataBitz) +* [@Hyperion2220](https://github.com/Hyperion2220) +* [@blassley](https://github.com/blassley) +* [@JanKleine](https://github.com/JanKleine) +* [@koiralasandesh](https://github.com/koiralasandesh) + +## What's Next + +Now that the post-merger feature freeze has ended, the team is resuming active feature development. Stay tuned to our blog for upcoming releases and in-depth looks at what we're building next. + +In the meantime, please upgrade to **v3.1.0** right away, especially if you are using a Plex Media Server configuration. See our [migration guide](https://docs.seerr.dev/migration-guide) if you need help upgrading from Overseerr/Jellyseerr. \ No newline at end of file diff --git a/package.json b/package.json index d68daad5d..97887d9c5 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "license": "MIT", "dependencies": { "@dr.pogodin/csurf": "^1.16.6", + "@fontsource-variable/inter": "^5.2.8", "@formatjs/intl-displaynames": "6.8.13", "@formatjs/intl-locale": "3.1.1", "@formatjs/intl-pluralrules": "5.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8933e1917..e48171c43 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,9 @@ importers: '@dr.pogodin/csurf': specifier: ^1.16.6 version: 1.16.6 + '@fontsource-variable/inter': + specifier: ^5.2.8 + version: 5.2.8 '@formatjs/intl-displaynames': specifier: 6.8.13 version: 6.8.13 @@ -1382,6 +1385,9 @@ packages: '@floating-ui/utils@0.2.10': resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@fontsource-variable/inter@5.2.8': + resolution: {integrity: sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ==} + '@formatjs/ecma402-abstract@1.14.3': resolution: {integrity: sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==} @@ -10118,6 +10124,8 @@ snapshots: '@floating-ui/utils@0.2.10': {} + '@fontsource-variable/inter@5.2.8': {} + '@formatjs/ecma402-abstract@1.14.3': dependencies: '@formatjs/intl-localematcher': 0.2.32 diff --git a/server/api/servarr/sonarr.ts b/server/api/servarr/sonarr.ts index 01b429ba5..76f1416d1 100644 --- a/server/api/servarr/sonarr.ts +++ b/server/api/servarr/sonarr.ts @@ -49,6 +49,7 @@ export interface SonarrSeries { languageProfileId: number; seasonFolder: boolean; monitored: boolean; + monitorNewItems: 'all' | 'none'; useSceneNumbering: boolean; runtime: number; tvdbId: number; @@ -98,6 +99,7 @@ export interface AddSeriesOptions { tags?: number[]; seriesType: SonarrSeries['seriesType']; monitored?: boolean; + monitorNewItems?: SonarrSeries['monitorNewItems']; searchNow?: boolean; } @@ -269,6 +271,7 @@ class SonarrAPI extends ServarrBase<{ tags: options.tags, seasonFolder: options.seasonFolder, monitored: options.monitored, + monitorNewItems: options.monitorNewItems, rootFolderPath: options.rootFolderPath, seriesType: options.seriesType, addOptions: { diff --git a/server/entity/User.ts b/server/entity/User.ts index b15b6683a..011b1be49 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -39,7 +39,16 @@ export class User { return users.map((u) => u.filter(showFiltered)); } - static readonly filteredFields: string[] = ['email', 'plexId']; + static readonly filteredFields: string[] = [ + 'email', + 'plexId', + 'password', + 'resetPasswordGuid', + 'jellyfinDeviceId', + 'jellyfinAuthToken', + 'plexToken', + 'settings', + ]; public displayName: string; diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index 2b7175eda..e4c106ae6 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -214,16 +214,13 @@ class DiscordAgent : undefined; return { - title: payload.subject, + title: payload.event + ? `${payload.event}: ${payload.subject}` + : payload.subject, url, description: payload.message, color, timestamp: new Date().toISOString(), - author: payload.event - ? { - name: payload.event, - } - : undefined, fields, thumbnail: embedPoster ? { diff --git a/server/lib/notifications/agents/ntfy.ts b/server/lib/notifications/agents/ntfy.ts index 561df2c84..a67373e82 100644 --- a/server/lib/notifications/agents/ntfy.ts +++ b/server/lib/notifications/agents/ntfy.ts @@ -27,7 +27,7 @@ class NtfyAgent const { embedPoster } = settings.notifications.agents.ntfy; const topic = this.getSettings().options.topic; - const priority = 3; + const priority = this.getSettings().options.priority ?? 3; const title = payload.event ? `${payload.event} - ${payload.subject}` diff --git a/server/lib/settings/index.ts b/server/lib/settings/index.ts index 1e659cd29..261e19b56 100644 --- a/server/lib/settings/index.ts +++ b/server/lib/settings/index.ts @@ -93,6 +93,7 @@ export interface SonarrSettings extends DVRSettings { activeLanguageProfileId?: number; animeTags?: number[]; enableSeasonFolders: boolean; + monitorNewItems: 'all' | 'none'; } interface Quota { @@ -297,6 +298,7 @@ export interface NotificationAgentNtfy extends NotificationAgentConfig { password?: string; authMethodToken?: boolean; token?: string; + priority?: number; }; } @@ -530,6 +532,7 @@ class Settings { options: { url: '', topic: '', + priority: 3, }, }, }, diff --git a/server/routes/auth.ts b/server/routes/auth.ts index b74befe5e..d625d85ec 100644 --- a/server/routes/auth.ts +++ b/server/routes/auth.ts @@ -244,8 +244,7 @@ authRoutes.post('/jellyfin', async (req, res, next) => { (settings.main.mediaServerLogin === false || // media server is neither jellyfin or emby (settings.main.mediaServerType !== MediaServerType.JELLYFIN && - settings.main.mediaServerType !== MediaServerType.EMBY && - settings.jellyfin.ip !== '')) + settings.main.mediaServerType !== MediaServerType.EMBY)) ) { return res.status(500).json({ error: 'Jellyfin login is disabled' }); } diff --git a/server/routes/imageproxy.ts b/server/routes/imageproxy.ts index ac2fbe082..ede3e83e3 100644 --- a/server/routes/imageproxy.ts +++ b/server/routes/imageproxy.ts @@ -32,6 +32,12 @@ function initTvdbImageProxy() { router.get('/:type/*', async (req, res) => { const imagePath = req.path.replace(/^\/\w+/, ''); + + if (imagePath.startsWith('//') || imagePath.includes('://')) { + logger.error('Invalid URL for image proxy', { imagePath }); + return res.status(403).send('Invalid URL for image proxy'); + } + try { let imageData; if (req.params.type === 'tmdb') { diff --git a/server/routes/issue.ts b/server/routes/issue.ts index 5b0bbd983..1e8769f8e 100644 --- a/server/routes/issue.ts +++ b/server/routes/issue.ts @@ -293,7 +293,7 @@ issueRoutes.post<{ issueId: string }, Issue, { message: string }>( }); issue.comments = [...issue.comments, comment]; - + issue.updatedAt = new Date(); await issueRepository.save(issue); return res.status(200).json(issue); diff --git a/server/routes/media.ts b/server/routes/media.ts index d7962825d..8bc1b8bae 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -17,7 +17,7 @@ import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; import type { FindOneOptions } from 'typeorm'; -import { In } from 'typeorm'; +import { In, IsNull, Not } from 'typeorm'; const mediaRoutes = Router(); @@ -68,12 +68,18 @@ mediaRoutes.get('/', async (req, res, next) => { }; } + let whereClause: FindOneOptions['where']; + if (statusFilter || req.query.sort === 'mediaAdded') { + whereClause = {}; + if (statusFilter) whereClause.status = statusFilter; + if (req.query.sort === 'mediaAdded') + whereClause.mediaAddedAt = Not(IsNull()); + } + try { const [media, mediaCount] = await mediaRepository.findAndCount({ order: sortFilter, - where: statusFilter && { - status: statusFilter, - }, + where: whereClause, take: pageSize, skip, }); diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index b7733c92a..64766bfa0 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -22,6 +22,8 @@ import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { getHostname } from '@server/utils/getHostname'; +import { normalizeJellyfinGuid } from '@server/utils/jellyfin'; +import { isOwnProfileOrAdmin } from '@server/utils/profileMiddleware'; import { Router } from 'express'; import gravatarUrl from 'gravatar-url'; import { findIndex, sortBy } from 'lodash'; @@ -275,15 +277,16 @@ router.post< } }); -router.get<{ userId: number }>( +router.get<{ userId: string }>( '/:userId/pushSubscriptions', + isOwnProfileOrAdmin(), async (req, res, next) => { try { const userPushSubRepository = getRepository(UserPushSubscription); const userPushSubs = await userPushSubRepository.find({ relations: { user: true }, - where: { user: { id: req.params.userId } }, + where: { user: { id: Number(req.params.userId) } }, }); return res.status(200).json(userPushSubs); @@ -293,8 +296,9 @@ router.get<{ userId: number }>( } ); -router.get<{ userId: number; endpoint: string }>( +router.get<{ userId: string; endpoint: string }>( '/:userId/pushSubscription/:endpoint', + isOwnProfileOrAdmin(), async (req, res, next) => { try { const userPushSubRepository = getRepository(UserPushSubscription); @@ -304,7 +308,7 @@ router.get<{ userId: number; endpoint: string }>( user: true, }, where: { - user: { id: req.params.userId }, + user: { id: Number(req.params.userId) }, endpoint: req.params.endpoint, }, }); @@ -316,8 +320,9 @@ router.get<{ userId: number; endpoint: string }>( } ); -router.delete<{ userId: number; endpoint: string }>( +router.delete<{ userId: string; endpoint: string }>( '/:userId/pushSubscription/:endpoint', + isOwnProfileOrAdmin(), async (req, res, next) => { try { const userPushSubRepository = getRepository(UserPushSubscription); @@ -325,7 +330,7 @@ router.delete<{ userId: number; endpoint: string }>( const userPushSub = await userPushSubRepository.findOne({ relations: { user: true }, where: { - user: { id: req.params.userId }, + user: { id: Number(req.params.userId) }, endpoint: req.params.endpoint, }, }); @@ -355,14 +360,14 @@ router.delete<{ userId: number; endpoint: string }>( router.get<{ id: string }>('/:id', async (req, res, next) => { try { const userRepository = getRepository(User); - const user = await userRepository.findOneOrFail({ where: { id: Number(req.params.id) }, }); - return res - .status(200) - .json(user.filter(req.user?.hasPermission(Permission.MANAGE_USERS))); + const isOwnProfile = req.user?.id === user.id; + const isAdmin = req.user?.hasPermission(Permission.MANAGE_USERS); + + return res.status(200).json(user.filter(isOwnProfile || isAdmin)); } catch (e) { next({ status: 404, message: 'User not found.' }); } @@ -675,10 +680,20 @@ router.post( jellyfinClient.setUserId(admin.jellyfinUserId ?? ''); const jellyfinUsers = await jellyfinClient.getUsers(); - for (const jellyfinUserId of body.jellyfinUserIds) { - const jellyfinUser = jellyfinUsers.users.find( - (user) => user.Id === jellyfinUserId - ); + const jellyfinUsersById = new Map( + jellyfinUsers.users.map((user) => [ + normalizeJellyfinGuid(user.Id), + user, + ]) + ); + + for (const rawJellyfinUserId of body.jellyfinUserIds) { + const jellyfinUserId = normalizeJellyfinGuid(rawJellyfinUserId); + if (!jellyfinUserId) { + continue; + } + + const jellyfinUser = jellyfinUsersById.get(jellyfinUserId); const user = await userRepository.findOne({ select: ['id', 'jellyfinUserId'], @@ -747,18 +762,8 @@ router.get<{ id: string }, QuotaResponse>( router.get<{ id: string }, UserWatchDataResponse>( '/:id/watch_data', + isOwnProfileOrAdmin(), async (req, res, next) => { - if ( - Number(req.params.id) !== req.user?.id && - !req.user?.hasPermission(Permission.ADMIN) - ) { - return next({ - status: 403, - message: - "You do not have permission to view this user's recently watched media.", - }); - } - const settings = getSettings().tautulli; if (!settings.hostname || !settings.port || !settings.apiKey) { diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 50ea7c5a5..bd5af746f 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -16,40 +16,15 @@ import logger from '@server/logger'; import { isAuthenticated } from '@server/middleware/auth'; import { ApiError } from '@server/types/error'; import { getHostname } from '@server/utils/getHostname'; +import { + isOwnProfile, + isOwnProfileOrAdmin, +} from '@server/utils/profileMiddleware'; import { Router } from 'express'; import net from 'net'; import { Not } from 'typeorm'; import { canMakePermissionsChange } from '.'; -const isOwnProfile = (): Middleware => { - return (req, res, next) => { - if (req.user?.id !== Number(req.params.id)) { - return next({ - status: 403, - message: "You do not have permission to view this user's settings.", - }); - } - next(); - }; -}; - -const isOwnProfileOrAdmin = (): Middleware => { - const authMiddleware: Middleware = (req, res, next) => { - if ( - !req.user?.hasPermission(Permission.MANAGE_USERS) && - req.user?.id !== Number(req.params.id) - ) { - return next({ - status: 403, - message: "You do not have permission to view this user's settings.", - }); - } - - next(); - }; - return authMiddleware; -}; - const userSettingsRoutes = Router({ mergeParams: true }); userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( diff --git a/server/subscriber/MediaRequestSubscriber.ts b/server/subscriber/MediaRequestSubscriber.ts index c54ee3399..404b36df7 100644 --- a/server/subscriber/MediaRequestSubscriber.ts +++ b/server/subscriber/MediaRequestSubscriber.ts @@ -446,13 +446,32 @@ export class MediaRequestSubscriber implements EntitySubscriberInterface { + return (req, res, next) => { + if (req.user?.id !== Number(req.params.id)) { + return next({ + status: 403, + message: "You do not have permission to view this user's settings.", + }); + } + next(); + }; +}; + +export const isOwnProfileOrAdmin = (): Middleware => { + const authMiddleware: Middleware = (req, res, next) => { + if ( + !req.user?.hasPermission(Permission.MANAGE_USERS) && + req.user?.id !== Number(req.params.id) + ) { + return next({ + status: 403, + message: "You do not have permission to view this user's settings.", + }); + } + + next(); + }; + return authMiddleware; +}; diff --git a/src/components/Common/SensitiveInput/index.tsx b/src/components/Common/SensitiveInput/index.tsx index 9b19e4d08..65f48d802 100644 --- a/src/components/Common/SensitiveInput/index.tsx +++ b/src/components/Common/SensitiveInput/index.tsx @@ -20,7 +20,7 @@ const SensitiveInput = ({ as = 'input', ...props }: SensitiveInputProps) => { ? props : { ...props, - as: props.type === 'textarea' && !isHidden ? 'textarea' : undefined, + as: props.type === 'textarea' ? 'textarea' : undefined, }; return ( <> @@ -32,11 +32,18 @@ const SensitiveInput = ({ as = 'input', ...props }: SensitiveInputProps) => { {...componentProps} className={`rounded-l-only ${componentProps.className ?? ''}`} type={ - isHidden - ? 'password' - : props.type !== 'password' - ? (props.type ?? 'text') - : 'text' + props.type === 'textarea' + ? undefined + : isHidden + ? 'password' + : props.type !== 'password' + ? (props.type ?? 'text') + : 'text' + } + style={ + props.type === 'textarea' && isHidden + ? { WebkitTextSecurity: 'disc', ...props.style } + : props.style } />