Skip to content

Commit

Permalink
fix(csrf): add a custom fetch utility to handle requests with CSRF token
Browse files Browse the repository at this point in the history
During the migration from Axios to fetch, we overlooked the fact that Axios automatically handled
CSRF tokens, while fetch does not. When CSRF protection was turned on, requests were failing with an
"invalid CSRF token" error for users accessing the app even via HTTPS. This commit introduces the
`apiFetch` utility to ensure that the CSRF token is included in all requests.

fix #1011
  • Loading branch information
Fallenbagel committed Oct 16, 2024
1 parent a0f80fe commit 4b8fc3b
Show file tree
Hide file tree
Showing 64 changed files with 383 additions and 255 deletions.
3 changes: 2 additions & 1 deletion src/components/Blacklist/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import Error from '@app/pages/_error';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import {
ChevronLeftIcon,
Expand Down Expand Up @@ -238,7 +239,7 @@ const BlacklistedItem = ({ item, revalidateList }: BlacklistedItemProps) => {
const removeFromBlacklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true);

const res = await fetch('/api/v1/blacklist/' + tmdbId, {
const res = await apiFetch('/api/v1/blacklist/' + tmdbId, {
method: 'DELETE',
});

Expand Down
3 changes: 2 additions & 1 deletion src/components/BlacklistBlock/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Button from '@app/components/Common/Button';
import Tooltip from '@app/components/Common/Tooltip';
import { useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { CalendarIcon, TrashIcon, UserIcon } from '@heroicons/react/24/solid';
import type { Blacklist } from '@server/entity/Blacklist';
Expand Down Expand Up @@ -35,7 +36,7 @@ const BlacklistBlock = ({
const removeFromBlacklist = async (tmdbId: number, title?: string) => {
setIsUpdating(true);

const res = await fetch('/api/v1/blacklist/' + tmdbId, {
const res = await apiFetch('/api/v1/blacklist/' + tmdbId, {
method: 'DELETE',
});

Expand Down
42 changes: 23 additions & 19 deletions src/components/Discover/CreateSlider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { sliderTitles } from '@app/components/Discover/constants';
import MediaSlider from '@app/components/MediaSlider';
import { WatchProviderSelector } from '@app/components/Selector';
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import type {
TmdbCompanySearchResponse,
Expand Down Expand Up @@ -76,7 +77,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {

const keywords = await Promise.all(
slider.data.split(',').map(async (keywordId) => {
const res = await fetch(`/api/v1/keyword/${keywordId}`);
const res = await apiFetch(`/api/v1/keyword/${keywordId}`);
const keyword: Keyword = await res.json();
return keyword;
})
Expand All @@ -95,7 +96,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
return;
}

const res = await fetch(
const res = await apiFetch(
`/api/v1/genres/${
slider.type === DiscoverSliderType.TMDB_MOVIE_GENRE ? 'movie' : 'tv'
}`
Expand All @@ -116,7 +117,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
return;
}

const res = await fetch(`/api/v1/studio/${slider.data}`);
const res = await apiFetch(`/api/v1/studio/${slider.data}`);
const studio: ProductionCompany = await res.json();

setDefaultDataValue([
Expand Down Expand Up @@ -160,7 +161,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
);

const loadKeywordOptions = async (inputValue: string) => {
const res = await fetch(
const res = await apiFetch(
`/api/v1/search/keyword?query=${encodeURIExtraParams(inputValue)}`,
{
headers: {
Expand All @@ -181,7 +182,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
return [];
}

const res = await fetch(
const res = await apiFetch(
`/api/v1/search/company?query=${encodeURIExtraParams(inputValue)}`,
{
headers: {
Expand All @@ -198,7 +199,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
};

const loadMovieGenreOptions = async () => {
const res = await fetch('/api/v1/discover/genreslider/movie');
const res = await apiFetch('/api/v1/discover/genreslider/movie');
const results: GenreSliderItem[] = await res.json();

return results.map((result) => ({
Expand All @@ -208,7 +209,7 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
};

const loadTvGenreOptions = async () => {
const res = await fetch('/api/v1/discover/genreslider/tv');
const res = await apiFetch('/api/v1/discover/genreslider/tv');
const results: GenreSliderItem[] = await res.json();

return results.map((result) => ({
Expand Down Expand Up @@ -306,20 +307,23 @@ const CreateSlider = ({ onCreate, slider }: CreateSliderProps) => {
onSubmit={async (values, { resetForm }) => {
try {
if (slider) {
const res = await fetch(`/api/v1/settings/discover/${slider.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: Number(values.sliderType),
title: values.title,
data: values.data,
}),
});
const res = await apiFetch(
`/api/v1/settings/discover/${slider.id}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: Number(values.sliderType),
title: values.title,
data: values.data,
}),
}
);
if (!res.ok) throw new Error();
} else {
const res = await fetch('/api/v1/settings/discover/add', {
const res = await apiFetch('/api/v1/settings/discover/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
3 changes: 2 additions & 1 deletion src/components/Discover/DiscoverSliderEdit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CreateSlider from '@app/components/Discover/CreateSlider';
import GenreTag from '@app/components/GenreTag';
import KeywordTag from '@app/components/KeywordTag';
import globalMessages from '@app/i18n/globalMessages';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
import {
Expand Down Expand Up @@ -77,7 +78,7 @@ const DiscoverSliderEdit = ({

const deleteSlider = async () => {
try {
const res = await fetch(`/api/v1/settings/discover/${slider.id}`, {
const res = await apiFetch(`/api/v1/settings/discover/${slider.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
Expand Down
5 changes: 3 additions & 2 deletions src/components/Discover/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import MediaSlider from '@app/components/MediaSlider';
import { encodeURIExtraParams } from '@app/hooks/useDiscover';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import {
Expand Down Expand Up @@ -75,7 +76,7 @@ const Discover = () => {

const updateSliders = async () => {
try {
const res = await fetch('/api/v1/settings/discover', {
const res = await apiFetch('/api/v1/settings/discover', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -100,7 +101,7 @@ const Discover = () => {

const resetSliders = async () => {
try {
const res = await fetch('/api/v1/settings/discover/reset', {
const res = await apiFetch('/api/v1/settings/discover/reset', {
method: 'GET',
});
if (!res.ok) throw new Error();
Expand Down
5 changes: 3 additions & 2 deletions src/components/IssueDetails/IssueComment/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Button from '@app/components/Common/Button';
import CachedImage from '@app/components/Common/CachedImage';
import Modal from '@app/components/Common/Modal';
import { Permission, useUser } from '@app/hooks/useUser';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react';
import { EllipsisVerticalIcon } from '@heroicons/react/24/solid';
Expand Down Expand Up @@ -48,7 +49,7 @@ const IssueComment = ({

const deleteComment = async () => {
try {
const res = await fetch(`/api/v1/issueComment/${comment.id}`, {
const res = await apiFetch(`/api/v1/issueComment/${comment.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
Expand Down Expand Up @@ -177,7 +178,7 @@ const IssueComment = ({
<Formik
initialValues={{ newMessage: comment.message }}
onSubmit={async (values) => {
const res = await fetch(
const res = await apiFetch(
`/api/v1/issueComment/${comment.id}`,
{
method: 'PUT',
Expand Down
9 changes: 5 additions & 4 deletions src/components/IssueDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import ErrorPage from '@app/pages/_error';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import {
Expand Down Expand Up @@ -121,7 +122,7 @@ const IssueDetails = () => {

const editFirstComment = async (newMessage: string) => {
try {
const res = await fetch(`/api/v1/issueComment/${firstComment.id}`, {
const res = await apiFetch(`/api/v1/issueComment/${firstComment.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
Expand All @@ -145,7 +146,7 @@ const IssueDetails = () => {

const updateIssueStatus = async (newStatus: 'open' | 'resolved') => {
try {
const res = await fetch(`/api/v1/issue/${issueData.id}/${newStatus}`, {
const res = await apiFetch(`/api/v1/issue/${issueData.id}/${newStatus}`, {
method: 'POST',
});
if (!res.ok) throw new Error();
Expand All @@ -165,7 +166,7 @@ const IssueDetails = () => {

const deleteIssue = async () => {
try {
const res = await fetch(`/api/v1/issue/${issueData.id}`, {
const res = await apiFetch(`/api/v1/issue/${issueData.id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error();
Expand Down Expand Up @@ -499,7 +500,7 @@ const IssueDetails = () => {
}}
validationSchema={CommentSchema}
onSubmit={async (values, { resetForm }) => {
const res = await fetch(
const res = await apiFetch(
`/api/v1/issue/${issueData?.id}/comment`,
{
method: 'POST',
Expand Down
3 changes: 2 additions & 1 deletion src/components/IssueModal/CreateIssueModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { issueOptions } from '@app/components/IssueModal/constants';
import useSettings from '@app/hooks/useSettings';
import { Permission, useUser } from '@app/hooks/useUser';
import globalMessages from '@app/i18n/globalMessages';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { RadioGroup } from '@headlessui/react';
import { ArrowRightCircleIcon } from '@heroicons/react/24/solid';
Expand Down Expand Up @@ -100,7 +101,7 @@ const CreateIssueModal = ({
validationSchema={CreateIssueModalSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/issue', {
const res = await apiFetch('/api/v1/issue', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
3 changes: 2 additions & 1 deletion src/components/Layout/UserDropdown/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import CachedImage from '@app/components/Common/CachedImage';
import MiniQuotaDisplay from '@app/components/Layout/UserDropdown/MiniQuotaDisplay';
import { useUser } from '@app/hooks/useUser';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { Menu, Transition } from '@headlessui/react';
import {
Expand Down Expand Up @@ -38,7 +39,7 @@ const UserDropdown = () => {
const { user, revalidate } = useUser();

const logout = async () => {
const res = await fetch('/api/v1/auth/logout', {
const res = await apiFetch('/api/v1/auth/logout', {
method: 'POST',
});
if (!res.ok) throw new Error();
Expand Down
3 changes: 2 additions & 1 deletion src/components/Login/AddEmailModal.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Modal from '@app/components/Common/Modal';
import useSettings from '@app/hooks/useSettings';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { Field, Formik } from 'formik';
Expand Down Expand Up @@ -57,7 +58,7 @@ const AddEmailModal: React.FC<AddEmailModalProps> = ({
validationSchema={EmailSettingsSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/auth/jellyfin', {
const res = await apiFetch('/api/v1/auth/jellyfin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
6 changes: 4 additions & 2 deletions src/components/Login/JellyfinLogin.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Button from '@app/components/Common/Button';
import Tooltip from '@app/components/Common/Tooltip';
import useSettings from '@app/hooks/useSettings';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { InformationCircleIcon } from '@heroicons/react/24/solid';
import { ApiErrorCode } from '@server/constants/error';
Expand Down Expand Up @@ -114,8 +115,9 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
// throw new Error('Invalid serverType'); // You can customize the error message
// }

const res = await fetch('/api/v1/auth/jellyfin', {
const res = await apiFetch('/api/v1/auth/jellyfin', {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
Expand Down Expand Up @@ -370,7 +372,7 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
validationSchema={LoginSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/auth/jellyfin', {
const res = await apiFetch('/api/v1/auth/jellyfin', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
3 changes: 2 additions & 1 deletion src/components/Login/LocalLogin.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Button from '@app/components/Common/Button';
import SensitiveInput from '@app/components/Common/SensitiveInput';
import useSettings from '@app/hooks/useSettings';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import {
ArrowLeftOnRectangleIcon,
Expand Down Expand Up @@ -55,7 +56,7 @@ const LocalLogin = ({ revalidate }: LocalLoginProps) => {
validationSchema={LoginSchema}
onSubmit={async (values) => {
try {
const res = await fetch('/api/v1/auth/local', {
const res = await apiFetch('/api/v1/auth/local', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
3 changes: 2 additions & 1 deletion src/components/Login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import LocalLogin from '@app/components/Login/LocalLogin';
import PlexLoginButton from '@app/components/PlexLoginButton';
import useSettings from '@app/hooks/useSettings';
import { useUser } from '@app/hooks/useUser';
import apiFetch from '@app/utils/apiFetch';
import defineMessages from '@app/utils/defineMessages';
import { Transition } from '@headlessui/react';
import { XCircleIcon } from '@heroicons/react/24/solid';
Expand Down Expand Up @@ -41,7 +42,7 @@ const Login = () => {
const login = async () => {
setProcessing(true);
try {
const res = await fetch('/api/v1/auth/plex', {
const res = await apiFetch('/api/v1/auth/plex', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down
Loading

0 comments on commit 4b8fc3b

Please sign in to comment.