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

* refactor(ui): add tabs to user notification settings * feat(notif): allow users to enable/disable specific agents * fix(ui): only enforce required fields when agent is enabled * fix(ui): hide unavailable notification agents * feat(notif): mention admin users for admin Discord notifications * fix(ui): modify styling of PGP key textareas to suit expected input * fix(notif): mention all admins when there are multiple and fix rebase error * fix: add missing form values, and fix Yup validation * refactor: reduce repeated logic/code in email notif agent * refactor: move 'Notification Types' label into NotificationTypeSelector component * fix(email): correct inconsistencies in email template formatting * refactor: use bitfields for storing user-enabled notif agent types * feat: improve notification agent logging * fix(ui): mark string fields as nullable so empty values are not type errors * fix: add validation for PGP-related inputs * fix: correctly fetch user in user settings & log mentioned IDs for Discord notifs * fix(ui): fix mobile nav dropdown text & add hover effect to button-style tabs * fix(notif): process admin email notifications asynchronously * fix(logging): log name of notification type instead of its enum value * fix: mark required fields and pass all user settings values to API * fix(frontend): call mutate after changing email/Discord/Telegram global notif settings * refactor: get global notif settings from relevant API endpoints instead of adding to public settings * fix(notif): fall back to email notifications being enabled (default) if user settings do not exist * fix(notif): do not set notifyUser for MEDIA_PENDING or MEDIA_AUTO_APPROVED * fix: expose notif enabled settings in user notif endpoints & remove global enable notif setting * fix(notif): remove unnecessary allowed_mentions object from Discord payload * fix(notif): use form values for email test notification * fix: make suggested changes and regenerate DB migration * fix: loosen validation of PGP keys * fix: fix user profile settings routes * fix: remove route guard from profile pages
388 lines
11 KiB
TypeScript
388 lines
11 KiB
TypeScript
import { Router } from 'express';
|
|
import { getRepository } from 'typeorm';
|
|
import { canMakePermissionsChange } from '.';
|
|
import { User } from '../../entity/User';
|
|
import { UserSettings } from '../../entity/UserSettings';
|
|
import {
|
|
UserSettingsGeneralResponse,
|
|
UserSettingsNotificationsResponse,
|
|
} from '../../interfaces/api/userSettingsInterfaces';
|
|
import { NotificationAgentType } from '../../lib/notifications/agenttypes';
|
|
import { Permission } from '../../lib/permissions';
|
|
import { getSettings } from '../../lib/settings';
|
|
import logger from '../../logger';
|
|
import { isAuthenticated } from '../../middleware/auth';
|
|
|
|
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>(
|
|
'/main',
|
|
isOwnProfileOrAdmin(),
|
|
async (req, res, next) => {
|
|
const {
|
|
main: { defaultQuotas },
|
|
} = getSettings();
|
|
const userRepository = getRepository(User);
|
|
|
|
try {
|
|
const user = await userRepository.findOne({
|
|
where: { id: Number(req.params.id) },
|
|
});
|
|
|
|
if (!user) {
|
|
return next({ status: 404, message: 'User not found.' });
|
|
}
|
|
|
|
return res.status(200).json({
|
|
username: user.username,
|
|
region: user.settings?.region,
|
|
originalLanguage: user.settings?.originalLanguage,
|
|
movieQuotaLimit: user.movieQuotaLimit,
|
|
movieQuotaDays: user.movieQuotaDays,
|
|
tvQuotaLimit: user.tvQuotaLimit,
|
|
tvQuotaDays: user.tvQuotaDays,
|
|
globalMovieQuotaDays: defaultQuotas.movie.quotaDays,
|
|
globalMovieQuotaLimit: defaultQuotas.movie.quotaLimit,
|
|
globalTvQuotaDays: defaultQuotas.tv.quotaDays,
|
|
globalTvQuotaLimit: defaultQuotas.tv.quotaLimit,
|
|
});
|
|
} catch (e) {
|
|
next({ status: 500, message: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
userSettingsRoutes.post<
|
|
{ id: string },
|
|
UserSettingsGeneralResponse,
|
|
UserSettingsGeneralResponse
|
|
>('/main', isOwnProfileOrAdmin(), async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
|
|
try {
|
|
const user = await userRepository.findOne({
|
|
where: { id: Number(req.params.id) },
|
|
});
|
|
|
|
if (!user) {
|
|
return next({ status: 404, message: 'User not found.' });
|
|
}
|
|
|
|
// "Owner" user settings cannot be modified by other users
|
|
if (user.id === 1 && req.user?.id !== 1) {
|
|
return next({
|
|
status: 403,
|
|
message: "You do not have permission to modify this user's settings.",
|
|
});
|
|
}
|
|
|
|
user.username = req.body.username;
|
|
|
|
// Update quota values only if the user has the correct permissions
|
|
if (
|
|
!user.hasPermission(Permission.MANAGE_USERS) &&
|
|
req.user?.id !== user.id
|
|
) {
|
|
user.movieQuotaDays = req.body.movieQuotaDays;
|
|
user.movieQuotaLimit = req.body.movieQuotaLimit;
|
|
user.tvQuotaDays = req.body.tvQuotaDays;
|
|
user.tvQuotaLimit = req.body.tvQuotaLimit;
|
|
}
|
|
|
|
if (!user.settings) {
|
|
user.settings = new UserSettings({
|
|
user: req.user,
|
|
region: req.body.region,
|
|
originalLanguage: req.body.originalLanguage,
|
|
});
|
|
} else {
|
|
user.settings.region = req.body.region;
|
|
user.settings.originalLanguage = req.body.originalLanguage;
|
|
}
|
|
|
|
await userRepository.save(user);
|
|
|
|
return res.status(200).json({ username: user.username });
|
|
} catch (e) {
|
|
next({ status: 500, message: e.message });
|
|
}
|
|
});
|
|
|
|
userSettingsRoutes.get<{ id: string }, { hasPassword: boolean }>(
|
|
'/password',
|
|
isOwnProfileOrAdmin(),
|
|
async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
|
|
try {
|
|
const user = await userRepository.findOne({
|
|
where: { id: Number(req.params.id) },
|
|
select: ['id', 'password'],
|
|
});
|
|
|
|
if (!user) {
|
|
return next({ status: 404, message: 'User not found.' });
|
|
}
|
|
|
|
return res.status(200).json({ hasPassword: !!user.password });
|
|
} catch (e) {
|
|
next({ status: 500, message: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
userSettingsRoutes.post<
|
|
{ id: string },
|
|
null,
|
|
{ currentPassword?: string; newPassword: string }
|
|
>('/password', isOwnProfileOrAdmin(), async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
|
|
try {
|
|
const user = await userRepository.findOne({
|
|
where: { id: Number(req.params.id) },
|
|
});
|
|
|
|
const userWithPassword = await userRepository.findOne({
|
|
select: ['id', 'password'],
|
|
where: { id: Number(req.params.id) },
|
|
});
|
|
|
|
if (!user || !userWithPassword) {
|
|
return next({ status: 404, message: 'User not found.' });
|
|
}
|
|
|
|
if (req.body.newPassword.length < 8) {
|
|
return next({
|
|
status: 400,
|
|
message: 'Password must be at least 8 characters.',
|
|
});
|
|
}
|
|
|
|
if (
|
|
(user.id === 1 && req.user?.id !== 1) ||
|
|
(user.hasPermission(Permission.ADMIN) &&
|
|
user.id !== req.user?.id &&
|
|
req.user?.id !== 1)
|
|
) {
|
|
return next({
|
|
status: 403,
|
|
message: "You do not have permission to modify this user's password.",
|
|
});
|
|
}
|
|
|
|
// If the user has the permission to manage users and they are not
|
|
// editing themselves, we will just set the new password
|
|
if (
|
|
req.user?.hasPermission(Permission.MANAGE_USERS) &&
|
|
req.user?.id !== user.id
|
|
) {
|
|
await user.setPassword(req.body.newPassword);
|
|
await userRepository.save(user);
|
|
logger.debug('Password overriden by user.', {
|
|
label: 'User Settings',
|
|
userEmail: user.email,
|
|
changingUser: req.user.email,
|
|
});
|
|
return res.status(204).send();
|
|
}
|
|
|
|
// If the user has a password, we need to check the currentPassword is correct
|
|
if (
|
|
user.password &&
|
|
(!req.body.currentPassword ||
|
|
!(await userWithPassword.passwordMatch(req.body.currentPassword)))
|
|
) {
|
|
logger.debug(
|
|
'Attempt to change password for user failed. Invalid current password provided.',
|
|
{ label: 'User Settings', userEmail: user.email }
|
|
);
|
|
return next({ status: 403, message: 'Current password is invalid.' });
|
|
}
|
|
|
|
await user.setPassword(req.body.newPassword);
|
|
await userRepository.save(user);
|
|
|
|
return res.status(204).send();
|
|
} catch (e) {
|
|
next({ status: 500, message: e.message });
|
|
}
|
|
});
|
|
|
|
userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>(
|
|
'/notifications',
|
|
isOwnProfileOrAdmin(),
|
|
async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
const settings = getSettings();
|
|
|
|
try {
|
|
const user = await userRepository.findOne({
|
|
where: { id: Number(req.params.id) },
|
|
});
|
|
|
|
if (!user) {
|
|
return next({ status: 404, message: 'User not found.' });
|
|
}
|
|
|
|
return res.status(200).json({
|
|
notificationAgents:
|
|
user.settings?.notificationAgents ?? NotificationAgentType.EMAIL,
|
|
emailEnabled: settings?.notifications.agents.email.enabled,
|
|
pgpKey: user.settings?.pgpKey,
|
|
discordEnabled: settings?.notifications.agents.discord.enabled,
|
|
discordId: user.settings?.discordId,
|
|
telegramEnabled: settings?.notifications.agents.telegram.enabled,
|
|
telegramBotUsername:
|
|
settings?.notifications.agents.telegram.options.botUsername,
|
|
telegramChatId: user.settings?.telegramChatId,
|
|
telegramSendSilently: user?.settings?.telegramSendSilently,
|
|
});
|
|
} catch (e) {
|
|
next({ status: 500, message: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
userSettingsRoutes.post<{ id: string }, UserSettingsNotificationsResponse>(
|
|
'/notifications',
|
|
isOwnProfileOrAdmin(),
|
|
async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
|
|
try {
|
|
const user = await userRepository.findOne({
|
|
where: { id: Number(req.params.id) },
|
|
});
|
|
|
|
if (!user) {
|
|
return next({ status: 404, message: 'User not found.' });
|
|
}
|
|
|
|
// "Owner" user settings cannot be modified by other users
|
|
if (user.id === 1 && req.user?.id !== 1) {
|
|
return next({
|
|
status: 403,
|
|
message: "You do not have permission to modify this user's settings.",
|
|
});
|
|
}
|
|
|
|
if (!user.settings) {
|
|
user.settings = new UserSettings({
|
|
user: req.user,
|
|
notificationAgents:
|
|
req.body.notificationAgents ?? NotificationAgentType.EMAIL,
|
|
pgpKey: req.body.pgpKey,
|
|
discordId: req.body.discordId,
|
|
telegramChatId: req.body.telegramChatId,
|
|
telegramSendSilently: req.body.telegramSendSilently,
|
|
});
|
|
} else {
|
|
user.settings.notificationAgents =
|
|
req.body.notificationAgents ?? NotificationAgentType.EMAIL;
|
|
user.settings.pgpKey = req.body.pgpKey;
|
|
user.settings.discordId = req.body.discordId;
|
|
user.settings.telegramChatId = req.body.telegramChatId;
|
|
user.settings.telegramSendSilently = req.body.telegramSendSilently;
|
|
}
|
|
|
|
userRepository.save(user);
|
|
|
|
return res.status(200).json({
|
|
notificationAgents: user.settings?.notificationAgents,
|
|
pgpKey: user.settings?.pgpKey,
|
|
discordId: user.settings?.discordId,
|
|
telegramChatId: user.settings?.telegramChatId,
|
|
telegramSendSilently: user?.settings?.telegramSendSilently,
|
|
});
|
|
} catch (e) {
|
|
next({ status: 500, message: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
userSettingsRoutes.get<{ id: string }, { permissions?: number }>(
|
|
'/permissions',
|
|
isAuthenticated(Permission.MANAGE_USERS),
|
|
async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
|
|
try {
|
|
const user = await userRepository.findOne({
|
|
where: { id: Number(req.params.id) },
|
|
});
|
|
|
|
if (!user) {
|
|
return next({ status: 404, message: 'User not found.' });
|
|
}
|
|
|
|
return res.status(200).json({ permissions: user.permissions });
|
|
} catch (e) {
|
|
next({ status: 500, message: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
userSettingsRoutes.post<
|
|
{ id: string },
|
|
{ permissions?: number },
|
|
{ permissions: number }
|
|
>(
|
|
'/permissions',
|
|
isAuthenticated(Permission.MANAGE_USERS),
|
|
async (req, res, next) => {
|
|
const userRepository = getRepository(User);
|
|
|
|
try {
|
|
const user = await userRepository.findOne({
|
|
where: { id: Number(req.params.id) },
|
|
});
|
|
|
|
if (!user) {
|
|
return next({ status: 404, message: 'User not found.' });
|
|
}
|
|
|
|
// "Owner" user permissions cannot be modified, and users cannot set their own permissions
|
|
if (user.id === 1 || req.user?.id === user.id) {
|
|
return next({
|
|
status: 403,
|
|
message: 'You do not have permission to modify this user',
|
|
});
|
|
}
|
|
|
|
if (!canMakePermissionsChange(req.body.permissions, req.user)) {
|
|
return next({
|
|
status: 403,
|
|
message: 'You do not have permission to grant this level of access',
|
|
});
|
|
}
|
|
user.permissions = req.body.permissions;
|
|
|
|
await userRepository.save(user);
|
|
|
|
return res.status(200).json({ permissions: user.permissions });
|
|
} catch (e) {
|
|
next({ status: 500, message: e.message });
|
|
}
|
|
}
|
|
);
|
|
|
|
export default userSettingsRoutes;
|