mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00

CDNs such as Cloudflare bypass their cache if cookies are set in the response. clearCookies middleware removes the header before imageproxy serves the image.
227 lines
7.0 KiB
TypeScript
227 lines
7.0 KiB
TypeScript
import PlexAPI from '@server/api/plexapi';
|
|
import dataSource, { getRepository } from '@server/datasource';
|
|
import DiscoverSlider from '@server/entity/DiscoverSlider';
|
|
import { Session } from '@server/entity/Session';
|
|
import { User } from '@server/entity/User';
|
|
import { startJobs } from '@server/job/schedule';
|
|
import notificationManager from '@server/lib/notifications';
|
|
import DiscordAgent from '@server/lib/notifications/agents/discord';
|
|
import EmailAgent from '@server/lib/notifications/agents/email';
|
|
import GotifyAgent from '@server/lib/notifications/agents/gotify';
|
|
import LunaSeaAgent from '@server/lib/notifications/agents/lunasea';
|
|
import PushbulletAgent from '@server/lib/notifications/agents/pushbullet';
|
|
import PushoverAgent from '@server/lib/notifications/agents/pushover';
|
|
import SlackAgent from '@server/lib/notifications/agents/slack';
|
|
import TelegramAgent from '@server/lib/notifications/agents/telegram';
|
|
import WebhookAgent from '@server/lib/notifications/agents/webhook';
|
|
import WebPushAgent from '@server/lib/notifications/agents/webpush';
|
|
import { getSettings } from '@server/lib/settings';
|
|
import logger from '@server/logger';
|
|
import clearCookies from '@server/middleware/clearcookies';
|
|
import routes from '@server/routes';
|
|
import imageproxy from '@server/routes/imageproxy';
|
|
import { getAppVersion } from '@server/utils/appVersion';
|
|
import restartFlag from '@server/utils/restartFlag';
|
|
import { getClientIp } from '@supercharge/request-ip';
|
|
import { TypeormStore } from 'connect-typeorm/out';
|
|
import cookieParser from 'cookie-parser';
|
|
import csurf from 'csurf';
|
|
import type { NextFunction, Request, Response } from 'express';
|
|
import express from 'express';
|
|
import * as OpenApiValidator from 'express-openapi-validator';
|
|
import type { Store } from 'express-session';
|
|
import session from 'express-session';
|
|
import next from 'next';
|
|
import path from 'path';
|
|
import swaggerUi from 'swagger-ui-express';
|
|
import YAML from 'yamljs';
|
|
|
|
const API_SPEC_PATH = path.join(__dirname, '../overseerr-api.yml');
|
|
|
|
logger.info(`Starting Overseerr version ${getAppVersion()}`);
|
|
const dev = process.env.NODE_ENV !== 'production';
|
|
const app = next({ dev });
|
|
const handle = app.getRequestHandler();
|
|
|
|
app
|
|
.prepare()
|
|
.then(async () => {
|
|
const dbConnection = await dataSource.initialize();
|
|
|
|
// Run migrations in production
|
|
if (process.env.NODE_ENV === 'production') {
|
|
await dbConnection.query('PRAGMA foreign_keys=OFF');
|
|
await dbConnection.runMigrations();
|
|
await dbConnection.query('PRAGMA foreign_keys=ON');
|
|
}
|
|
|
|
// Load Settings
|
|
const settings = getSettings().load();
|
|
restartFlag.initializeSettings(settings.main);
|
|
|
|
// Migrate library types
|
|
if (
|
|
settings.plex.libraries.length > 1 &&
|
|
!settings.plex.libraries[0].type
|
|
) {
|
|
const userRepository = getRepository(User);
|
|
const admin = await userRepository.findOne({
|
|
select: { id: true, plexToken: true },
|
|
where: { id: 1 },
|
|
});
|
|
|
|
if (admin) {
|
|
logger.info('Migrating Plex libraries to include media type', {
|
|
label: 'Settings',
|
|
});
|
|
|
|
const plexapi = new PlexAPI({ plexToken: admin.plexToken });
|
|
await plexapi.syncLibraries();
|
|
}
|
|
}
|
|
|
|
// Register Notification Agents
|
|
notificationManager.registerAgents([
|
|
new DiscordAgent(),
|
|
new EmailAgent(),
|
|
new GotifyAgent(),
|
|
new LunaSeaAgent(),
|
|
new PushbulletAgent(),
|
|
new PushoverAgent(),
|
|
new SlackAgent(),
|
|
new TelegramAgent(),
|
|
new WebhookAgent(),
|
|
new WebPushAgent(),
|
|
]);
|
|
|
|
// Start Jobs
|
|
startJobs();
|
|
|
|
// Bootstrap Discovery Sliders
|
|
await DiscoverSlider.bootstrapSliders();
|
|
|
|
const server = express();
|
|
if (settings.main.trustProxy) {
|
|
server.enable('trust proxy');
|
|
}
|
|
server.use(cookieParser());
|
|
server.use(express.json());
|
|
server.use(express.urlencoded({ extended: true }));
|
|
server.use((req, _res, next) => {
|
|
try {
|
|
const descriptor = Object.getOwnPropertyDescriptor(req, 'ip');
|
|
if (descriptor?.writable === true) {
|
|
req.ip = getClientIp(req) ?? '';
|
|
}
|
|
} catch (e) {
|
|
logger.error('Failed to attach the ip to the request', {
|
|
label: 'Middleware',
|
|
message: e.message,
|
|
});
|
|
} finally {
|
|
next();
|
|
}
|
|
});
|
|
if (settings.main.csrfProtection) {
|
|
server.use(
|
|
csurf({
|
|
cookie: {
|
|
httpOnly: true,
|
|
sameSite: true,
|
|
secure: !dev,
|
|
},
|
|
})
|
|
);
|
|
server.use((req, res, next) => {
|
|
res.cookie('XSRF-TOKEN', req.csrfToken(), {
|
|
sameSite: true,
|
|
secure: !dev,
|
|
});
|
|
next();
|
|
});
|
|
}
|
|
|
|
// Set up sessions
|
|
const sessionRespository = getRepository(Session);
|
|
server.use(
|
|
'/api',
|
|
session({
|
|
secret: settings.clientId,
|
|
resave: false,
|
|
saveUninitialized: false,
|
|
cookie: {
|
|
maxAge: 1000 * 60 * 60 * 24 * 30,
|
|
httpOnly: true,
|
|
sameSite: true,
|
|
secure: 'auto',
|
|
},
|
|
store: new TypeormStore({
|
|
cleanupLimit: 2,
|
|
ttl: 1000 * 60 * 60 * 24 * 30,
|
|
}).connect(sessionRespository) as Store,
|
|
})
|
|
);
|
|
const apiDocs = YAML.load(API_SPEC_PATH);
|
|
server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDocs));
|
|
server.use(
|
|
OpenApiValidator.middleware({
|
|
apiSpec: API_SPEC_PATH,
|
|
validateRequests: true,
|
|
})
|
|
);
|
|
/**
|
|
* This is a workaround to convert dates to strings before they are validated by
|
|
* OpenAPI validator. Otherwise, they are treated as objects instead of strings
|
|
* and response validation will fail
|
|
*/
|
|
server.use((_req, res, next) => {
|
|
const original = res.json;
|
|
res.json = function jsonp(json) {
|
|
return original.call(this, JSON.parse(JSON.stringify(json)));
|
|
};
|
|
next();
|
|
});
|
|
server.use('/api/v1', routes);
|
|
|
|
// Do not set cookies so CDNs can cache them
|
|
server.use('/imageproxy', clearCookies, imageproxy);
|
|
|
|
server.get('*', (req, res) => handle(req, res));
|
|
server.use(
|
|
(
|
|
err: { status: number; message: string; errors: string[] },
|
|
_req: Request,
|
|
res: Response,
|
|
// We must provide a next function for the function signature here even though its not used
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
_next: NextFunction
|
|
) => {
|
|
// format error
|
|
res.status(err.status || 500).json({
|
|
message: err.message,
|
|
errors: err.errors,
|
|
});
|
|
}
|
|
);
|
|
|
|
const port = Number(process.env.PORT) || 5055;
|
|
const host = process.env.HOST;
|
|
if (host) {
|
|
server.listen(port, host, () => {
|
|
logger.info(`Server ready on ${host} port ${port}`, {
|
|
label: 'Server',
|
|
});
|
|
});
|
|
} else {
|
|
server.listen(port, () => {
|
|
logger.info(`Server ready on port ${port}`, {
|
|
label: 'Server',
|
|
});
|
|
});
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
logger.error(err.stack);
|
|
process.exit(1);
|
|
});
|