From 46c4ee1625cf3e74bd885ecfc254b1e46cf44f29 Mon Sep 17 00:00:00 2001 From: TheCatLady <52870424+TheCatLady@users.noreply.github.com> Date: Mon, 12 Apr 2021 23:31:31 -0400 Subject: [PATCH] feat(notif): allow users to enable/disable specific agents (#1172) * 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 --- overseerr-api.yml | 66 +-- server/entity/MediaRequest.ts | 4 +- server/entity/User.ts | 5 +- server/entity/UserSettings.ts | 28 +- .../interfaces/api/userSettingsInterfaces.ts | 9 +- server/lib/email/index.ts | 9 +- server/lib/notifications/agents/discord.ts | 58 +- server/lib/notifications/agents/email.ts | 531 ++++++------------ server/lib/notifications/agents/pushbullet.ts | 15 +- server/lib/notifications/agents/pushover.ts | 14 +- server/lib/notifications/agents/slack.ts | 18 +- server/lib/notifications/agents/telegram.ts | 121 ++-- server/lib/notifications/agents/webhook.ts | 11 +- server/lib/notifications/agenttypes.ts | 16 + server/lib/notifications/index.ts | 5 +- ...-AddUserSettingsNotificationAgentsField.ts | 52 ++ server/routes/settings/notifications.ts | 26 +- server/routes/user/usersettings.ts | 109 ++-- .../email/generatedpassword/html.pug | 3 +- server/templates/email/resetpassword/html.pug | 3 +- server/templates/email/test-email/html.pug | 3 +- src/components/Common/SettingsTabs/index.tsx | 173 ++++++ .../NotificationType/index.tsx | 4 +- .../NotificationTypeSelector/index.tsx | 31 +- .../Notifications/NotificationsDiscord.tsx | 36 +- .../Notifications/NotificationsEmail.tsx | 147 +++-- .../NotificationsPushbullet/index.tsx | 41 +- .../NotificationsPushover/index.tsx | 49 +- .../NotificationsSlack/index.tsx | 37 +- .../Notifications/NotificationsTelegram.tsx | 55 +- .../NotificationsWebhook/index.tsx | 49 +- src/components/Settings/SettingsLayout.tsx | 81 +-- .../Settings/SettingsNotifications.tsx | 182 +----- .../UserNotificationsDiscord.tsx | 178 ++++++ .../UserNotificationsEmail.tsx | 175 ++++++ .../UserNotificationsTelegram.tsx | 217 +++++++ .../UserNotificationSettings/index.tsx | 306 +++------- .../UserProfile/UserSettings/index.tsx | 127 +---- src/context/SettingsContext.tsx | 2 +- src/hooks/useUser.ts | 1 - src/i18n/locale/en.json | 69 +-- src/pages/profile/settings/notifications.tsx | 14 - .../settings/notifications/discord.tsx | 17 + .../profile/settings/notifications/email.tsx | 17 + .../settings/notifications/telegram.tsx | 17 + .../users/[userId]/settings/notifications.tsx | 17 - .../settings/notifications/discord.tsx | 20 + .../[userId]/settings/notifications/email.tsx | 20 + .../settings/notifications/telegram.tsx | 20 + src/styles/globals.css | 20 +- 50 files changed, 1727 insertions(+), 1501 deletions(-) create mode 100644 server/lib/notifications/agenttypes.ts create mode 100644 server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts create mode 100644 src/components/Common/SettingsTabs/index.tsx create mode 100644 src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx create mode 100644 src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx create mode 100644 src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx delete mode 100644 src/pages/profile/settings/notifications.tsx create mode 100644 src/pages/profile/settings/notifications/discord.tsx create mode 100644 src/pages/profile/settings/notifications/email.tsx create mode 100644 src/pages/profile/settings/notifications/telegram.tsx delete mode 100644 src/pages/users/[userId]/settings/notifications.tsx create mode 100644 src/pages/users/[userId]/settings/notifications/discord.tsx create mode 100644 src/pages/users/[userId]/settings/notifications/email.tsx create mode 100644 src/pages/users/[userId]/settings/notifications/telegram.tsx diff --git a/overseerr-api.yml b/overseerr-api.yml index 965a903ca..08bf1b5ca 100644 --- a/overseerr-api.yml +++ b/overseerr-api.yml @@ -92,17 +92,12 @@ components: UserSettings: type: object properties: - enableNotifications: - type: boolean - default: true discordId: type: string - telegramChatId: + region: + type: string + language: type: string - telegramSendSilently: - type: boolean - required: - - enableNotifications MainSettings: type: object properties: @@ -1201,12 +1196,6 @@ components: type: string priority: type: number - NotificationSettings: - type: object - properties: - enabled: - type: boolean - example: true NotificationEmailSettings: type: object properties: @@ -1559,20 +1548,30 @@ components: UserSettingsNotifications: type: object properties: - enableNotifications: + notificationAgents: + type: number + example: 0 + emailEnabled: + type: boolean + pgpKey: + type: string + nullable: true + discordEnabled: type: boolean - default: true discordId: type: string nullable: true + telegramEnabled: + type: boolean + telegramBotUsername: + type: string + nullable: true telegramChatId: type: string nullable: true telegramSendSilently: type: boolean nullable: true - required: - - enableNotifications securitySchemes: cookieAuth: type: apiKey @@ -2306,37 +2305,6 @@ paths: timestamp: type: string example: 2020-12-15T16:20:00.069Z - /settings/notifications: - get: - summary: Return notification settings - description: Returns current notification settings in a JSON object. - tags: - - settings - responses: - '200': - description: Returned settings - content: - application/json: - schema: - $ref: '#/components/schemas/NotificationSettings' - post: - summary: Update notification settings - description: Updates notification settings with the provided values. - tags: - - settings - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/NotificationSettings' - responses: - '200': - description: 'Values were sucessfully updated' - content: - application/json: - schema: - $ref: '#/components/schemas/NotificationSettings' /settings/notifications/email: get: summary: Get email notification settings diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index ebc9b9fd3..167d1db06 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -145,7 +145,6 @@ export class MediaRequest { subject: movie.title, message: movie.overview, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, - notifyUser: this.requestedBy, media, request: this, }); @@ -157,7 +156,6 @@ export class MediaRequest { subject: tv.name, message: tv.overview, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, - notifyUser: this.requestedBy, media, extra: [ { @@ -232,7 +230,7 @@ export class MediaRequest { subject: tv.name, message: tv.overview, image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, - notifyUser: this.requestedBy, + notifyUser: autoApproved ? undefined : this.requestedBy, media, extra: [ { diff --git a/server/entity/User.ts b/server/entity/User.ts index 5c3927060..25b57f716 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -157,7 +157,8 @@ export class User { logger.info(`Sending generated password email for ${this.email}`, { label: 'User Management', }); - const email = new PreparedEmail(); + + const email = new PreparedEmail(getSettings().notifications.agents.email); await email.send({ template: path.join(__dirname, '../templates/email/generatedpassword'), message: { @@ -193,7 +194,7 @@ export class User { logger.info(`Sending reset password email for ${this.email}`, { label: 'User Management', }); - const email = new PreparedEmail(); + const email = new PreparedEmail(getSettings().notifications.agents.email); await email.send({ template: path.join(__dirname, '../templates/email/resetpassword'), message: { diff --git a/server/entity/UserSettings.ts b/server/entity/UserSettings.ts index 8e60865a6..023a1bde7 100644 --- a/server/entity/UserSettings.ts +++ b/server/entity/UserSettings.ts @@ -5,6 +5,10 @@ import { OneToOne, PrimaryGeneratedColumn, } from 'typeorm'; +import { + hasNotificationAgentEnabled, + NotificationAgentType, +} from '../lib/notifications/agenttypes'; import { User } from './User'; @Entity() @@ -20,8 +24,17 @@ export class UserSettings { @JoinColumn() public user: User; - @Column({ default: true }) - public enableNotifications: boolean; + @Column({ nullable: true }) + public region?: string; + + @Column({ nullable: true }) + public originalLanguage?: string; + + @Column({ type: 'integer', default: NotificationAgentType.EMAIL }) + public notificationAgents = NotificationAgentType.EMAIL; + + @Column({ nullable: true }) + public pgpKey?: string; @Column({ nullable: true }) public discordId?: string; @@ -32,12 +45,7 @@ export class UserSettings { @Column({ nullable: true }) public telegramSendSilently?: boolean; - @Column({ nullable: true }) - public region?: string; - - @Column({ nullable: true }) - public originalLanguage?: string; - - @Column({ nullable: true }) - public pgpKey?: string; + public hasNotificationAgentEnabled(agent: NotificationAgentType): boolean { + return !!hasNotificationAgentEnabled(agent, this.notificationAgents); + } } diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index e6d0302fe..006facf00 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -13,10 +13,13 @@ export interface UserSettingsGeneralResponse { } export interface UserSettingsNotificationsResponse { - enableNotifications: boolean; - telegramBotUsername?: string; + notificationAgents: number; + emailEnabled?: boolean; + pgpKey?: string; + discordEnabled?: boolean; discordId?: string; + telegramEnabled?: boolean; + telegramBotUsername?: string; telegramChatId?: string; telegramSendSilently?: boolean; - pgpKey?: string; } diff --git a/server/lib/email/index.ts b/server/lib/email/index.ts index abbc1632b..f9c0c7479 100644 --- a/server/lib/email/index.ts +++ b/server/lib/email/index.ts @@ -1,11 +1,10 @@ -import nodemailer from 'nodemailer'; import Email from 'email-templates'; -import { getSettings } from '../settings'; +import nodemailer from 'nodemailer'; +import { NotificationAgentEmail } from '../settings'; import { openpgpEncrypt } from './openpgpEncrypt'; -class PreparedEmail extends Email { - public constructor(pgpKey?: string) { - const settings = getSettings().notifications.agents.email; +class PreparedEmail extends Email { + public constructor(settings: NotificationAgentEmail, pgpKey?: string) { const transport = nodemailer.createTransport({ host: settings.options.smtpHost, port: settings.options.smtpPort, diff --git a/server/lib/notifications/agents/discord.ts b/server/lib/notifications/agents/discord.ts index cefde1861..c04b4948e 100644 --- a/server/lib/notifications/agents/discord.ts +++ b/server/lib/notifications/agents/discord.ts @@ -1,7 +1,11 @@ import axios from 'axios'; +import { getRepository } from 'typeorm'; import { hasNotificationType, Notification } from '..'; +import { User } from '../../../entity/User'; import logger from '../../../logger'; +import { Permission } from '../../permissions'; import { getSettings, NotificationAgentDiscord } from '../../settings'; +import { NotificationAgentType } from '../agenttypes'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; enum EmbedColors { @@ -107,7 +111,7 @@ class DiscordAgent if (payload.request) { fields.push({ name: 'Requested By', - value: payload.request?.requestedBy.displayName ?? '', + value: payload.request.requestedBy.displayName, inline: true, }); } @@ -201,7 +205,14 @@ class DiscordAgent type: Notification, payload: NotificationPayload ): Promise { - logger.debug('Sending Discord notification', { label: 'Notifications' }); + logger.debug('Sending Discord notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + }); + + let content = undefined; + try { const { botUsername, @@ -213,16 +224,32 @@ class DiscordAgent return false; } - const mentionedUsers: string[] = []; - let content = undefined; + if (payload.notifyUser) { + // Mention user who submitted the request + if ( + payload.notifyUser.settings?.hasNotificationAgentEnabled( + NotificationAgentType.DISCORD + ) && + payload.notifyUser.settings?.discordId + ) { + content = `<@${payload.notifyUser.settings.discordId}>`; + } + } else { + // Mention all users with the Manage Requests permission + const userRepository = getRepository(User); + const users = await userRepository.find(); - if ( - payload.notifyUser && - (payload.notifyUser.settings?.enableNotifications ?? true) && - payload.notifyUser.settings?.discordId - ) { - mentionedUsers.push(payload.notifyUser.settings.discordId); - content = `<@${payload.notifyUser.settings.discordId}>`; + content = users + .filter( + (user) => + user.hasPermission(Permission.MANAGE_REQUESTS) && + user.settings?.hasNotificationAgentEnabled( + NotificationAgentType.DISCORD + ) && + user.settings?.discordId + ) + .map((user) => `<@${user.settings?.discordId}>`) + .join(' '); } await axios.post(webhookUrl, { @@ -230,18 +257,19 @@ class DiscordAgent avatar_url: botAvatarUrl, embeds: [this.buildEmbed(type, payload)], content, - allowed_mentions: { - users: mentionedUsers, - }, } as DiscordWebhookPayload); return true; } catch (e) { logger.error('Error sending Discord notification', { label: 'Notifications', - message: e.message, + mentions: content, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, response: e.response.data, }); + return false; } } diff --git a/server/lib/notifications/agents/email.ts b/server/lib/notifications/agents/email.ts index ea6b02ef5..4d00eb6f2 100644 --- a/server/lib/notifications/agents/email.ts +++ b/server/lib/notifications/agents/email.ts @@ -1,3 +1,4 @@ +import { EmailOptions } from 'email-templates'; import path from 'path'; import { getRepository } from 'typeorm'; import { hasNotificationType, Notification } from '..'; @@ -7,6 +8,7 @@ import logger from '../../../logger'; import PreparedEmail from '../../email'; import { Permission } from '../../permissions'; import { getSettings, NotificationAgentEmail } from '../../settings'; +import { NotificationAgentType } from '../agenttypes'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; class EmailAgent @@ -35,379 +37,194 @@ class EmailAgent return false; } - private async sendMediaRequestEmail(payload: NotificationPayload) { - // This is getting main settings for the whole app + private buildMessage( + type: Notification, + payload: NotificationPayload, + toEmail: string + ): EmailOptions | undefined { const { applicationUrl, applicationTitle } = getSettings().main; - try { - const userRepository = getRepository(User); - const users = await userRepository.find(); - // Send to all users with the manage requests permission (or admins) - users - .filter( - (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && - (user.settings?.enableNotifications ?? true) - ) - .forEach((user) => { - const email = new PreparedEmail(user.settings?.pgpKey); - - email.send({ - template: path.join( - __dirname, - '../../../templates/email/media-request' - ), - message: { - to: user.email, - }, - locals: { - body: `A user has requested a new ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - }!`, - mediaName: payload.subject, - mediaPlot: payload.message, - mediaExtra: payload.extra ?? [], - imageUrl: payload.image, - timestamp: new Date().toTimeString(), - requestedBy: payload.request?.requestedBy.displayName, - actionUrl: applicationUrl - ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` - : undefined, - applicationUrl, - applicationTitle, - requestType: `New ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`, - }, - }); - }); - return true; - } catch (e) { - logger.error('Email notification failed to send', { - label: 'Notifications', - message: e.message, - }); - return false; + if (type === Notification.TEST_NOTIFICATION) { + return { + template: path.join(__dirname, '../../../templates/email/test-email'), + message: { + to: toEmail, + }, + locals: { + body: payload.message, + applicationUrl, + applicationTitle, + }, + }; } - } - private async sendMediaFailedEmail(payload: NotificationPayload) { - // This is getting main settings for the whole app - const { applicationUrl, applicationTitle } = getSettings().main; - try { - const userRepository = getRepository(User); - const users = await userRepository.find(); + if (payload.media) { + let requestType = ''; + let body = ''; - // Send to all users with the manage requests permission (or admins) - users - .filter( - (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && - (user.settings?.enableNotifications ?? true) - ) - .forEach((user) => { - const email = new PreparedEmail(user.settings?.pgpKey); - - email.send({ - template: path.join( - __dirname, - '../../../templates/email/media-request' - ), - message: { - to: user.email, - }, - locals: { - body: `A new request for the following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } could not be added to ${ - payload.media?.mediaType === MediaType.TV ? 'Sonarr' : 'Radarr' - }:`, - mediaName: payload.subject, - mediaPlot: payload.message, - imageUrl: payload.image, - timestamp: new Date().toTimeString(), - requestedBy: payload.request?.requestedBy.displayName, - actionUrl: applicationUrl - ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` - : undefined, - applicationUrl, - applicationTitle, - requestType: `Failed ${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request`, - }, - }); - }); - return true; - } catch (e) { - logger.error('Email notification failed to send', { - label: 'Notifications', - message: e.message, - }); - return false; - } - } - - private async sendMediaApprovedEmail(payload: NotificationPayload) { - // This is getting main settings for the whole app - const { applicationUrl, applicationTitle } = getSettings().main; - try { - if ( - payload.notifyUser && - (payload.notifyUser.settings?.enableNotifications ?? true) - ) { - const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey); - - await email.send({ - template: path.join( - __dirname, - '../../../templates/email/media-request' - ), - message: { - to: payload.notifyUser.email, - }, - locals: { - body: `Your request for the following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } has been approved:`, - mediaName: payload.subject, - mediaExtra: payload.extra ?? [], - imageUrl: payload.image, - timestamp: new Date().toTimeString(), - requestedBy: payload.request?.requestedBy.displayName, - actionUrl: applicationUrl - ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` - : undefined, - applicationUrl, - applicationTitle, - requestType: `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Approved`, - }, - }); + switch (type) { + case Notification.MEDIA_PENDING: + requestType = `New ${ + payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' + } Request`; + body = `A user has requested a new ${ + payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' + }!`; + break; + case Notification.MEDIA_APPROVED: + requestType = `${ + payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' + } Request Approved`; + body = `Your request for the following ${ + payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' + } has been approved:`; + break; + case Notification.MEDIA_AUTO_APPROVED: + requestType = `${ + payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' + } Request Automatically Approved`; + body = `A new request for the following ${ + payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' + } has been automatically approved:`; + break; + case Notification.MEDIA_AVAILABLE: + requestType = `${ + payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' + } Now Available`; + body = `The following ${ + payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' + } you requested is now available!`; + break; + case Notification.MEDIA_DECLINED: + requestType = `${ + payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' + } Request Declined`; + body = `Your request for the following ${ + payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' + } was declined:`; + break; + case Notification.MEDIA_FAILED: + requestType = `Failed ${ + payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' + } Request`; + body = `A new request for the following ${ + payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' + } could not be added to ${ + payload.media?.mediaType === MediaType.TV ? 'Sonarr' : 'Radarr' + }:`; + break; } - return true; - } catch (e) { - logger.error('Email notification failed to send', { - label: 'Notifications', - message: e.message, - }); - return false; + return { + template: path.join( + __dirname, + '../../../templates/email/media-request' + ), + message: { + to: toEmail, + }, + locals: { + requestType, + body, + mediaName: payload.subject, + mediaPlot: payload.message, + mediaExtra: payload.extra ?? [], + imageUrl: payload.image, + timestamp: new Date().toTimeString(), + requestedBy: payload.request?.requestedBy.displayName, + actionUrl: applicationUrl + ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` + : undefined, + applicationUrl, + applicationTitle, + }, + }; } - } - private async sendMediaAutoApprovedEmail(payload: NotificationPayload) { - // This is getting main settings for the whole app - const { applicationUrl, applicationTitle } = getSettings().main; - try { - const userRepository = getRepository(User); - const users = await userRepository.find(); - - // Send to all users with the manage requests permission (or admins) - users - .filter( - (user) => - user.hasPermission(Permission.MANAGE_REQUESTS) && - (user.settings?.enableNotifications ?? true) - ) - .forEach((user) => { - const email = new PreparedEmail(); - - email.send({ - template: path.join( - __dirname, - '../../../templates/email/media-request' - ), - message: { - to: user.email, - }, - locals: { - body: `A new request for the following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } has been automatically approved:`, - mediaName: payload.subject, - mediaExtra: payload.extra ?? [], - imageUrl: payload.image, - timestamp: new Date().toTimeString(), - requestedBy: payload.request?.requestedBy.displayName, - actionUrl: applicationUrl - ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` - : undefined, - applicationUrl, - applicationTitle, - requestType: `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Automatically Approved`, - }, - }); - }); - return true; - } catch (e) { - logger.error('Email notification failed to send', { - label: 'Notifications', - message: e.message, - }); - return false; - } - } - - private async sendMediaDeclinedEmail(payload: NotificationPayload) { - // This is getting main settings for the whole app - const { applicationUrl, applicationTitle } = getSettings().main; - try { - if ( - payload.notifyUser && - (payload.notifyUser.settings?.enableNotifications ?? true) - ) { - const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey); - - await email.send({ - template: path.join( - __dirname, - '../../../templates/email/media-request' - ), - message: { - to: payload.notifyUser.email, - }, - locals: { - body: `Your request for the following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } was declined:`, - mediaName: payload.subject, - mediaExtra: payload.extra ?? [], - imageUrl: payload.image, - timestamp: new Date().toTimeString(), - requestedBy: payload.request?.requestedBy.displayName, - actionUrl: applicationUrl - ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` - : undefined, - applicationUrl, - applicationTitle, - requestType: `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Request Declined`, - }, - }); - } - - return true; - } catch (e) { - logger.error('Email notification failed to send', { - label: 'Notifications', - message: e.message, - }); - return false; - } - } - - private async sendMediaAvailableEmail(payload: NotificationPayload) { - // This is getting main settings for the whole app - const { applicationUrl, applicationTitle } = getSettings().main; - try { - if ( - payload.notifyUser && - (payload.notifyUser.settings?.enableNotifications ?? true) - ) { - const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey); - - await email.send({ - template: path.join( - __dirname, - '../../../templates/email/media-request' - ), - message: { - to: payload.notifyUser.email, - }, - locals: { - body: `The following ${ - payload.media?.mediaType === MediaType.TV ? 'series' : 'movie' - } you requested is now available!`, - mediaName: payload.subject, - mediaExtra: payload.extra ?? [], - imageUrl: payload.image, - timestamp: new Date().toTimeString(), - requestedBy: payload.request?.requestedBy.displayName, - actionUrl: applicationUrl - ? `${applicationUrl}/${payload.media?.mediaType}/${payload.media?.tmdbId}` - : undefined, - applicationUrl, - applicationTitle, - requestType: `${ - payload.media?.mediaType === MediaType.TV ? 'Series' : 'Movie' - } Now Available`, - }, - }); - } - - return true; - } catch (e) { - logger.error('Email notification failed to send', { - label: 'Notifications', - message: e.message, - }); - return false; - } - } - - private async sendTestEmail(payload: NotificationPayload) { - // This is getting main settings for the whole app - const { applicationUrl, applicationTitle } = getSettings().main; - try { - if (payload.notifyUser) { - const email = new PreparedEmail(payload.notifyUser.settings?.pgpKey); - - await email.send({ - template: path.join(__dirname, '../../../templates/email/test-email'), - message: { - to: payload.notifyUser.email, - }, - locals: { - body: payload.message, - applicationUrl, - applicationTitle, - }, - }); - } - - return true; - } catch (e) { - logger.error('Email notification failed to send', { - label: 'Notifications', - message: e.message, - }); - return false; - } + return undefined; } public async send( type: Notification, payload: NotificationPayload ): Promise { - logger.debug('Sending email notification', { label: 'Notifications' }); + if (payload.notifyUser) { + // Send notification to the user who submitted the request + if ( + !payload.notifyUser.settings || + payload.notifyUser.settings.hasNotificationAgentEnabled( + NotificationAgentType.EMAIL + ) + ) { + logger.debug('Sending email notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + }); - switch (type) { - case Notification.MEDIA_PENDING: - this.sendMediaRequestEmail(payload); - break; - case Notification.MEDIA_APPROVED: - this.sendMediaApprovedEmail(payload); - break; - case Notification.MEDIA_AUTO_APPROVED: - this.sendMediaAutoApprovedEmail(payload); - break; - case Notification.MEDIA_DECLINED: - this.sendMediaDeclinedEmail(payload); - break; - case Notification.MEDIA_AVAILABLE: - this.sendMediaAvailableEmail(payload); - break; - case Notification.MEDIA_FAILED: - this.sendMediaFailedEmail(payload); - break; - case Notification.TEST_NOTIFICATION: - this.sendTestEmail(payload); - break; + try { + const email = new PreparedEmail( + this.getSettings(), + payload.notifyUser.settings?.pgpKey + ); + await email.send( + this.buildMessage(type, payload, payload.notifyUser.email) + ); + } catch (e) { + logger.error('Error sending email notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + }); + + return false; + } + } + } else { + // Send notifications to all users with the Manage Requests permission + const userRepository = getRepository(User); + const users = await userRepository.find(); + + await Promise.all( + users + .filter( + (user) => + user.hasPermission(Permission.MANAGE_REQUESTS) && + (!user.settings || + user.settings.hasNotificationAgentEnabled( + NotificationAgentType.EMAIL + )) + ) + .map(async (user) => { + logger.debug('Sending email notification', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + }); + + try { + const email = new PreparedEmail( + this.getSettings(), + user.settings?.pgpKey + ); + await email.send(this.buildMessage(type, payload, user.email)); + } catch (e) { + logger.error('Error sending email notification', { + label: 'Notifications', + recipient: user.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + }); + + return false; + } + }) + ); } return true; diff --git a/server/lib/notifications/agents/pushbullet.ts b/server/lib/notifications/agents/pushbullet.ts index f0c0f757e..c43e99711 100644 --- a/server/lib/notifications/agents/pushbullet.ts +++ b/server/lib/notifications/agents/pushbullet.ts @@ -1,9 +1,9 @@ import axios from 'axios'; import { hasNotificationType, Notification } from '..'; +import { MediaType } from '../../../constants/media'; import logger from '../../../logger'; import { getSettings, NotificationAgentPushbullet } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; -import { MediaType } from '../../../constants/media'; interface PushbulletPayload { title: string; @@ -136,7 +136,12 @@ class PushbulletAgent type: Notification, payload: NotificationPayload ): Promise { - logger.debug('Sending Pushbullet notification', { label: 'Notifications' }); + logger.debug('Sending Pushbullet notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + }); + try { const endpoint = 'https://api.pushbullet.com/v2/pushes'; @@ -162,8 +167,12 @@ class PushbulletAgent } catch (e) { logger.error('Error sending Pushbullet notification', { label: 'Notifications', - message: e.message, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response.data, }); + return false; } } diff --git a/server/lib/notifications/agents/pushover.ts b/server/lib/notifications/agents/pushover.ts index 3b5d3f873..f9bff21c3 100644 --- a/server/lib/notifications/agents/pushover.ts +++ b/server/lib/notifications/agents/pushover.ts @@ -1,9 +1,9 @@ import axios from 'axios'; import { hasNotificationType, Notification } from '..'; +import { MediaType } from '../../../constants/media'; import logger from '../../../logger'; import { getSettings, NotificationAgentPushover } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; -import { MediaType } from '../../../constants/media'; interface PushoverPayload { token: string; @@ -160,7 +160,11 @@ class PushoverAgent type: Notification, payload: NotificationPayload ): Promise { - logger.debug('Sending Pushover notification', { label: 'Notifications' }); + logger.debug('Sending Pushover notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + }); try { const endpoint = 'https://api.pushover.net/1/messages.json'; @@ -189,8 +193,12 @@ class PushoverAgent } catch (e) { logger.error('Error sending Pushover notification', { label: 'Notifications', - message: e.message, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response.data, }); + return false; } } diff --git a/server/lib/notifications/agents/slack.ts b/server/lib/notifications/agents/slack.ts index b52347854..f9fe46c9d 100644 --- a/server/lib/notifications/agents/slack.ts +++ b/server/lib/notifications/agents/slack.ts @@ -1,9 +1,9 @@ import axios from 'axios'; import { hasNotificationType, Notification } from '..'; +import { MediaType } from '../../../constants/media'; import logger from '../../../logger'; import { getSettings, NotificationAgentSlack } from '../../settings'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; -import { MediaType } from '../../../constants/media'; interface EmbedField { type: 'plain_text' | 'mrkdwn'; @@ -67,9 +67,7 @@ class SlackAgent if (payload.request) { fields.push({ type: 'mrkdwn', - text: `*Requested By*\n${ - payload.request?.requestedBy.displayName ?? '' - }`, + text: `*Requested By*\n${payload.request.requestedBy.displayName}`, }); } @@ -235,7 +233,11 @@ class SlackAgent type: Notification, payload: NotificationPayload ): Promise { - logger.debug('Sending Slack notification', { label: 'Notifications' }); + logger.debug('Sending Slack notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + }); try { const webhookUrl = this.getSettings().options.webhookUrl; @@ -249,8 +251,12 @@ class SlackAgent } catch (e) { logger.error('Error sending Slack notification', { label: 'Notifications', - message: e.message, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response.data, }); + return false; } } diff --git a/server/lib/notifications/agents/telegram.ts b/server/lib/notifications/agents/telegram.ts index 5fa4c5189..894a77262 100644 --- a/server/lib/notifications/agents/telegram.ts +++ b/server/lib/notifications/agents/telegram.ts @@ -3,6 +3,7 @@ import { hasNotificationType, Notification } from '..'; import { MediaType } from '../../../constants/media'; import logger from '../../../logger'; import { getSettings, NotificationAgentTelegram } from '../../settings'; +import { NotificationAgentType } from '../agenttypes'; import { BaseAgent, NotificationAgent, NotificationPayload } from './agent'; interface TelegramMessagePayload { @@ -155,62 +156,98 @@ class TelegramAgent type: Notification, payload: NotificationPayload ): Promise { - logger.debug('Sending Telegram notification', { label: 'Notifications' }); + const endpoint = `${this.baseUrl}bot${this.getSettings().options.botAPI}/${ + payload.image ? 'sendPhoto' : 'sendMessage' + }`; + + // Send system notification try { - const endpoint = `${this.baseUrl}bot${ - this.getSettings().options.botAPI - }/${payload.image ? 'sendPhoto' : 'sendMessage'}`; + logger.debug('Sending Telegram notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + }); - // Send system notification - await (payload.image - ? axios.post(endpoint, { - photo: payload.image, - caption: this.buildMessage(type, payload), - parse_mode: 'MarkdownV2', - chat_id: `${this.getSettings().options.chatId}`, - disable_notification: this.getSettings().options.sendSilently, - } as TelegramPhotoPayload) - : axios.post(endpoint, { - text: this.buildMessage(type, payload), - parse_mode: 'MarkdownV2', - chat_id: `${this.getSettings().options.chatId}`, - disable_notification: this.getSettings().options.sendSilently, - } as TelegramMessagePayload)); - - // Send user notification - if ( - payload.notifyUser && - (payload.notifyUser.settings?.enableNotifications ?? true) && - payload.notifyUser.settings?.telegramChatId && - payload.notifyUser.settings?.telegramChatId !== - this.getSettings().options.chatId - ) { - await (payload.image - ? axios.post(endpoint, { + await axios.post( + endpoint, + payload.image + ? ({ photo: payload.image, caption: this.buildMessage(type, payload), parse_mode: 'MarkdownV2', - chat_id: `${payload.notifyUser.settings.telegramChatId}`, - disable_notification: - payload.notifyUser.settings.telegramSendSilently, + chat_id: this.getSettings().options.chatId, + disable_notification: this.getSettings().options.sendSilently, } as TelegramPhotoPayload) - : axios.post(endpoint, { + : ({ text: this.buildMessage(type, payload), parse_mode: 'MarkdownV2', - chat_id: `${payload.notifyUser.settings.telegramChatId}`, - disable_notification: - payload.notifyUser.settings.telegramSendSilently, - } as TelegramMessagePayload)); - } - - return true; + chat_id: `${this.getSettings().options.chatId}`, + disable_notification: this.getSettings().options.sendSilently, + } as TelegramMessagePayload) + ); } catch (e) { logger.error('Error sending Telegram notification', { label: 'Notifications', - message: e.message, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response.data, }); return false; } + + if ( + payload.notifyUser && + payload.notifyUser.settings?.hasNotificationAgentEnabled( + NotificationAgentType.TELEGRAM + ) && + payload.notifyUser.settings?.telegramChatId && + payload.notifyUser.settings?.telegramChatId !== + this.getSettings().options.chatId + ) { + // Send notification to the user who submitted the request + logger.debug('Sending Telegram notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + }); + + try { + await axios.post( + endpoint, + payload.image + ? ({ + photo: payload.image, + caption: this.buildMessage(type, payload), + parse_mode: 'MarkdownV2', + chat_id: payload.notifyUser.settings.telegramChatId, + disable_notification: + payload.notifyUser.settings.telegramSendSilently, + } as TelegramPhotoPayload) + : ({ + text: this.buildMessage(type, payload), + parse_mode: 'MarkdownV2', + chat_id: payload.notifyUser.settings.telegramChatId, + disable_notification: + payload.notifyUser.settings.telegramSendSilently, + } as TelegramMessagePayload) + ); + } catch (e) { + logger.error('Error sending Telegram notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, + errorMessage: e.message, + response: e.response.data, + }); + + return false; + } + } + + return true; } } diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index b5c989233..7630cf443 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -128,7 +128,12 @@ class WebhookAgent type: Notification, payload: NotificationPayload ): Promise { - logger.debug('Sending webhook notification', { label: 'Notifications' }); + logger.debug('Sending webhook notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + }); + try { const { webhookUrl, authHeader } = this.getSettings().options; @@ -146,8 +151,12 @@ class WebhookAgent } catch (e) { logger.error('Error sending webhook notification', { label: 'Notifications', + type: Notification[type], + subject: payload.subject, errorMessage: e.message, + response: e.response.data, }); + return false; } } diff --git a/server/lib/notifications/agenttypes.ts b/server/lib/notifications/agenttypes.ts new file mode 100644 index 000000000..9e0d79aa8 --- /dev/null +++ b/server/lib/notifications/agenttypes.ts @@ -0,0 +1,16 @@ +export enum NotificationAgentType { + NONE = 0, + EMAIL = 2, + DISCORD = 4, + TELEGRAM = 8, + PUSHOVER = 16, + PUSHBULLET = 32, + SLACK = 64, +} + +export const hasNotificationAgentEnabled = ( + agent: NotificationAgentType, + value: number +): boolean => { + return !!(value & agent); +}; diff --git a/server/lib/notifications/index.ts b/server/lib/notifications/index.ts index 7d5b68003..f1f237f5e 100644 --- a/server/lib/notifications/index.ts +++ b/server/lib/notifications/index.ts @@ -38,7 +38,7 @@ class NotificationManager { public registerAgents = (agents: NotificationAgent[]): void => { this.activeAgents = [...this.activeAgents, ...agents]; - logger.info('Registered Notification Agents', { label: 'Notifications' }); + logger.info('Registered notification agents', { label: 'Notifications' }); }; public sendNotification( @@ -46,8 +46,9 @@ class NotificationManager { payload: NotificationPayload ): void { const settings = getSettings().notifications; - logger.info(`Sending notification for ${Notification[type]}`, { + logger.info(`Sending notification(s) for ${Notification[type]}`, { label: 'Notifications', + subject: payload.subject, }); this.activeAgents.forEach((agent) => { if (settings.enabled && agent.shouldSend(type)) { diff --git a/server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts b/server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts new file mode 100644 index 000000000..86a52c089 --- /dev/null +++ b/server/migration/1617730837489-AddUserSettingsNotificationAgentsField.ts @@ -0,0 +1,52 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUserSettingsNotificationAgentsField1617730837489 + implements MigrationInterface { + name = 'AddUserSettingsNotificationAgentsField1617730837489'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_settings"("id", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "user_settings"` + ); + await queryRunner.query(`DROP TABLE "user_settings"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"` + ); + await queryRunner.query( + `CREATE TABLE "temporary_user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" integer NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "user_settings"` + ); + await queryRunner.query(`DROP TABLE "user_settings"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_settings" RENAME TO "user_settings"` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"` + ); + await queryRunner.query( + `CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "notificationAgents" NOT NULL DEFAULT (2), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_settings"("id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "notificationAgents", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + await queryRunner.query( + `ALTER TABLE "user_settings" RENAME TO "temporary_user_settings"` + ); + await queryRunner.query( + `CREATE TABLE "user_settings" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "enableNotifications" boolean NOT NULL DEFAULT (1), "discordId" varchar, "userId" integer, "region" varchar, "originalLanguage" varchar, "telegramChatId" varchar, "telegramSendSilently" boolean, "pgpKey" varchar, CONSTRAINT "UQ_986a2b6d3c05eb4091bb8066f78" UNIQUE ("userId"), CONSTRAINT "FK_986a2b6d3c05eb4091bb8066f78" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_settings"("id", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey") SELECT "id", "discordId", "userId", "region", "originalLanguage", "telegramChatId", "telegramSendSilently", "pgpKey" FROM "temporary_user_settings"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_settings"`); + } +} diff --git a/server/routes/settings/notifications.ts b/server/routes/settings/notifications.ts index fbf1ce1e5..739b3981d 100644 --- a/server/routes/settings/notifications.ts +++ b/server/routes/settings/notifications.ts @@ -1,36 +1,16 @@ import { Router } from 'express'; -import { getSettings } from '../../lib/settings'; import { Notification } from '../../lib/notifications'; import DiscordAgent from '../../lib/notifications/agents/discord'; import EmailAgent from '../../lib/notifications/agents/email'; +import PushbulletAgent from '../../lib/notifications/agents/pushbullet'; +import PushoverAgent from '../../lib/notifications/agents/pushover'; import SlackAgent from '../../lib/notifications/agents/slack'; import TelegramAgent from '../../lib/notifications/agents/telegram'; -import PushoverAgent from '../../lib/notifications/agents/pushover'; import WebhookAgent from '../../lib/notifications/agents/webhook'; -import PushbulletAgent from '../../lib/notifications/agents/pushbullet'; +import { getSettings } from '../../lib/settings'; const notificationRoutes = Router(); -notificationRoutes.get('/', (_req, res) => { - const settings = getSettings().notifications; - return res.status(200).json({ - enabled: settings.enabled, - }); -}); - -notificationRoutes.post('/', (req, res) => { - const settings = getSettings(); - - Object.assign(settings.notifications, { - enabled: req.body.enabled, - }); - settings.save(); - - return res.status(200).json({ - enabled: settings.notifications.enabled, - }); -}); - notificationRoutes.get('/discord', (_req, res) => { const settings = getSettings(); diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index 693c228e5..f85ef1797 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -7,6 +7,7 @@ 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'; @@ -242,13 +243,17 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( } return res.status(200).json({ - enableNotifications: user.settings?.enableNotifications ?? true, + 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, - discordId: user.settings?.discordId, telegramChatId: user.settings?.telegramChatId, telegramSendSilently: user?.settings?.telegramSendSilently, - pgpKey: user?.settings?.pgpKey, }); } catch (e) { next({ status: 500, message: e.message }); @@ -256,60 +261,62 @@ userSettingsRoutes.get<{ id: string }, UserSettingsNotificationsResponse>( } ); -userSettingsRoutes.post< - { id: string }, - UserSettingsNotificationsResponse, - UserSettingsNotificationsResponse ->('/notifications', isOwnProfileOrAdmin(), async (req, res, next) => { - const userRepository = getRepository(User); +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.", + try { + const user = await userRepository.findOne({ + where: { id: Number(req.params.id) }, }); - } - if (!user.settings) { - user.settings = new UserSettings({ - user: req.user, - enableNotifications: req.body.enableNotifications, - discordId: req.body.discordId, - telegramChatId: req.body.telegramChatId, - telegramSendSilently: req.body.telegramSendSilently, - pgpKey: req.body.pgpKey, + 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, }); - } else { - user.settings.enableNotifications = req.body.enableNotifications; - user.settings.discordId = req.body.discordId; - user.settings.telegramChatId = req.body.telegramChatId; - user.settings.telegramSendSilently = req.body.telegramSendSilently; - user.settings.pgpKey = req.body.pgpKey; + } catch (e) { + next({ status: 500, message: e.message }); } - - userRepository.save(user); - - return res.status(200).json({ - enableNotifications: user.settings.enableNotifications, - discordId: user.settings.discordId, - telegramChatId: user.settings.telegramChatId, - telegramSendSilently: user.settings.telegramSendSilently, - pgpKey: user.settings.pgpKey, - }); - } catch (e) { - next({ status: 500, message: e.message }); } -}); +); userSettingsRoutes.get<{ id: string }, { permissions?: number }>( '/permissions', diff --git a/server/templates/email/generatedpassword/html.pug b/server/templates/email/generatedpassword/html.pug index 1fa4713f6..b9bc2a2e6 100644 --- a/server/templates/email/generatedpassword/html.pug +++ b/server/templates/email/generatedpassword/html.pug @@ -42,7 +42,6 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en') table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation') tr td(align='center' style='\ - font-size: 16px;\ padding-top: 25px;\ padding-bottom: 25px;\ text-align: center;\ @@ -50,7 +49,7 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en') a(href=applicationUrl style='\ text-shadow: 0 1px 0 #ffffff;\ font-weight: 700;\ - font-size: 16px;\ + font-size: 24px;\ color: #a8aaaf;\ text-decoration: none;\ ') diff --git a/server/templates/email/resetpassword/html.pug b/server/templates/email/resetpassword/html.pug index f7c8bb08d..718a0495a 100644 --- a/server/templates/email/resetpassword/html.pug +++ b/server/templates/email/resetpassword/html.pug @@ -42,7 +42,6 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en') table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation') tr td(align='center' style='\ - font-size: 16px;\ padding-top: 25px;\ padding-bottom: 25px;\ text-align: center;\ @@ -50,7 +49,7 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en') a(href=applicationUrl style='\ text-shadow: 0 1px 0 #ffffff;\ font-weight: 700;\ - font-size: 16px;\ + font-size: 24px;\ color: #a8aaaf;\ text-decoration: none;\ ') diff --git a/server/templates/email/test-email/html.pug b/server/templates/email/test-email/html.pug index b4abfebbf..f1b21b36e 100644 --- a/server/templates/email/test-email/html.pug +++ b/server/templates/email/test-email/html.pug @@ -42,7 +42,6 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en') table(style='width: 100%' width='100%' cellpadding='0' cellspacing='0' role='presentation') tr td(align='center' style='\ - font-size: 16px;\ padding-top: 25px;\ padding-bottom: 25px;\ text-align: center;\ @@ -50,7 +49,7 @@ div(role='article' aria-roledescription='email' aria-label='' lang='en') a(href=applicationUrl style='\ text-shadow: 0 1px 0 #ffffff;\ font-weight: 700;\ - font-size: 16px;\ + font-size: 24px;\ color: #a8aaaf;\ text-decoration: none;\ ') diff --git a/src/components/Common/SettingsTabs/index.tsx b/src/components/Common/SettingsTabs/index.tsx new file mode 100644 index 000000000..2e47b4183 --- /dev/null +++ b/src/components/Common/SettingsTabs/index.tsx @@ -0,0 +1,173 @@ +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import React from 'react'; +import { hasPermission, Permission } from '../../../../server/lib/permissions'; +import { useUser } from '../../../hooks/useUser'; + +export interface SettingsRoute { + text: string; + content?: React.ReactNode; + route: string; + regex: RegExp; + requiredPermission?: Permission | Permission[]; + permissionType?: { type: 'and' | 'or' }; + hidden?: boolean; +} + +const SettingsLink: React.FC<{ + tabType: 'default' | 'button'; + currentPath: string; + route: string; + regex: RegExp; + hidden?: boolean; + isMobile?: boolean; +}> = ({ + children, + tabType, + currentPath, + route, + regex, + hidden = false, + isMobile = false, +}) => { + if (hidden) { + return null; + } + + if (isMobile) { + return ; + } + + let linkClasses = + 'px-1 py-4 ml-8 text-sm font-medium leading-5 transition duration-300 border-b-2 border-transparent whitespace-nowrap first:ml-0'; + let activeLinkColor = 'text-indigo-500 border-indigo-600'; + let inactiveLinkColor = + 'text-gray-500 border-transparent hover:text-gray-300 hover:border-gray-400 focus:text-gray-300 focus:border-gray-400'; + + if (tabType === 'button') { + linkClasses = + 'px-3 py-2 ml-8 text-sm font-medium transition duration-300 rounded-md whitespace-nowrap first:ml-0'; + activeLinkColor = 'bg-indigo-700'; + inactiveLinkColor = 'bg-gray-800 hover:bg-gray-700 focus:bg-gray-700'; + } + + return ( + + + {children} + + + ); +}; + +const SettingsTabs: React.FC<{ + tabType?: 'default' | 'button'; + settingsRoutes: SettingsRoute[]; +}> = ({ tabType = 'default', settingsRoutes }) => { + const router = useRouter(); + const { user: currentUser } = useUser(); + + return ( + <> +
+ + +
+ {tabType === 'button' ? ( +
+ +
+ ) : ( +
+
+ +
+
+ )} + + ); +}; + +export default SettingsTabs; diff --git a/src/components/NotificationTypeSelector/NotificationType/index.tsx b/src/components/NotificationTypeSelector/NotificationType/index.tsx index 85224717e..4085b2a6d 100644 --- a/src/components/NotificationTypeSelector/NotificationType/index.tsx +++ b/src/components/NotificationTypeSelector/NotificationType/index.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { NotificationItem, hasNotificationType } from '..'; +import { hasNotificationType, NotificationItem } from '..'; interface NotificationTypeProps { option: NotificationItem; @@ -46,7 +46,7 @@ const NotificationType: React.FC = ({ />
-
-
-
- - {intl.formatMessage(messages.notificationtypes)} - * - -
-
- setFieldValue('types', newTypes)} - /> -
-
-
-
+ setFieldValue('types', newTypes)} + />
diff --git a/src/components/Settings/Notifications/NotificationsEmail.tsx b/src/components/Settings/Notifications/NotificationsEmail.tsx index 04e743239..625062bf9 100644 --- a/src/components/Settings/Notifications/NotificationsEmail.tsx +++ b/src/components/Settings/Notifications/NotificationsEmail.tsx @@ -13,7 +13,7 @@ import LoadingSpinner from '../../Common/LoadingSpinner'; import NotificationTypeSelector from '../../NotificationTypeSelector'; const messages = defineMessages({ - validationSmtpHostRequired: 'You must provide a hostname or IP address', + validationSmtpHostRequired: 'You must provide a valid hostname or IP address', validationSmtpPortRequired: 'You must provide a valid port number', agentenabled: 'Enable Agent', emailsender: 'Sender Address', @@ -24,34 +24,32 @@ const messages = defineMessages({ authPass: 'SMTP Password', emailsettingssaved: 'Email notification settings saved successfully!', emailsettingsfailed: 'Email notification settings failed to save.', - testsent: 'Test notification sent!', + testsent: 'Email test notification sent!', allowselfsigned: 'Allow Self-Signed Certificates', ssldisabletip: 'SSL should be disabled on standard TLS connections (port 587)', senderName: 'Sender Name', - notificationtypes: 'Notification Types', validationEmail: 'You must provide a valid email address', emailNotificationTypesAlert: 'Email Notification Recipients', emailNotificationTypesAlertDescription: 'Media Requested, Media Automatically Approved, and Media Failed email notifications are sent to all users with the Manage Requests permission.', emailNotificationTypesAlertDescriptionPt2: 'Media Approved, Media Declined, and Media Available email notifications are sent to the user who submitted the request.', - pgpPrivateKey: 'PGP Private Key', + pgpPrivateKey: 'PGP Private Key', pgpPrivateKeyTip: - 'Sign encrypted email messages (PGP password is also required)', - pgpPassword: 'PGP Password', + 'Sign encrypted email messages using OpenPGP', + validationPgpPrivateKey: + 'You must provide a valid PGP private key if a PGP password is entered', + pgpPassword: 'PGP Password', pgpPasswordTip: - 'Sign encrypted email messages (PGP private key is also required)', + 'Sign encrypted email messages using OpenPGP', + validationPgpPassword: + 'You must provide a PGP password if a PGP private key is entered', }); -export function PgpLink(msg: string): JSX.Element { +export function OpenPgpLink(msg: string): JSX.Element { return ( - + {msg} ); @@ -64,21 +62,60 @@ const NotificationsEmail: React.FC = () => { '/api/v1/settings/notifications/email' ); - const NotificationsEmailSchema = Yup.object().shape({ - emailFrom: Yup.string() - .required(intl.formatMessage(messages.validationEmail)) - .email(intl.formatMessage(messages.validationEmail)), - smtpHost: Yup.string() - .required(intl.formatMessage(messages.validationSmtpHostRequired)) - .matches( - // eslint-disable-next-line - /^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i, - intl.formatMessage(messages.validationSmtpHostRequired) - ), - smtpPort: Yup.number() - .typeError(intl.formatMessage(messages.validationSmtpPortRequired)) - .required(intl.formatMessage(messages.validationSmtpPortRequired)), - }); + const NotificationsEmailSchema = Yup.object().shape( + { + emailFrom: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationEmail)), + otherwise: Yup.string().nullable(), + }) + .email(intl.formatMessage(messages.validationEmail)), + smtpHost: Yup.string() + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationSmtpHostRequired)), + otherwise: Yup.string().nullable(), + }) + .matches( + /^(([a-z]|\d|_|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])$/i, + intl.formatMessage(messages.validationSmtpHostRequired) + ), + smtpPort: Yup.number() + .typeError(intl.formatMessage(messages.validationSmtpPortRequired)) + .when('enabled', { + is: true, + then: Yup.number().required( + intl.formatMessage(messages.validationSmtpPortRequired) + ), + otherwise: Yup.number().nullable(), + }), + pgpPrivateKey: Yup.string() + .when('pgpPassword', { + is: (value: unknown) => !!value, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationPgpPrivateKey)), + otherwise: Yup.string().nullable(), + }) + .matches( + /^-----BEGIN PGP PRIVATE KEY BLOCK-----.+-----END PGP PRIVATE KEY BLOCK-----$/, + intl.formatMessage(messages.validationPgpPrivateKey) + ), + pgpPassword: Yup.string().when('pgpPrivateKey', { + is: (value: unknown) => !!value, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationPgpPassword)), + otherwise: Yup.string().nullable(), + }), + }, + [['pgpPrivateKey', 'pgpPassword']] + ); if (!data && !error) { return ; @@ -119,6 +156,7 @@ const NotificationsEmail: React.FC = () => { pgpPassword: values.pgpPassword, }, }); + addToast(intl.formatMessage(messages.emailsettingssaved), { appearance: 'success', autoDismiss: true, @@ -323,15 +361,15 @@ const NotificationsEmail: React.FC = () => {
@@ -340,23 +378,27 @@ const NotificationsEmail: React.FC = () => { id="pgpPrivateKey" name="pgpPrivateKey" as="textarea" - rows="3" + rows="10" + className="font-mono text-xs" />
+ {errors.pgpPrivateKey && touched.pgpPrivateKey && ( +
{errors.pgpPrivateKey}
+ )}
@@ -368,30 +410,15 @@ const NotificationsEmail: React.FC = () => { autoComplete="off" />
+ {errors.pgpPassword && touched.pgpPassword && ( +
{errors.pgpPassword}
+ )}
-
-
- - {intl.formatMessage(messages.notificationtypes)} - * - -
-
- - setFieldValue('types', newTypes) - } - /> -
-
-
-
+ setFieldValue('types', newTypes)} + />
diff --git a/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx b/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx index f5e940fb9..dbb95afe5 100644 --- a/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx +++ b/src/components/Settings/Notifications/NotificationsPushbullet/index.tsx @@ -18,11 +18,10 @@ const messages = defineMessages({ pushbulletSettingsSaved: 'Pushbullet notification settings saved successfully!', pushbulletSettingsFailed: 'Pushbullet notification settings failed to save.', - testSent: 'Test notification sent!', + testSent: 'Pushbullet test notification sent!', settingUpPushbullet: 'Setting Up Pushbullet Notifications', settingUpPushbulletDescription: - 'To configure Pushbullet notifications, you will need to create an access token and enter it below.', - notificationTypes: 'Notification Types', + 'To configure Pushbullet notifications, you will need to create an access token.', }); const NotificationsPushbullet: React.FC = () => { @@ -33,9 +32,13 @@ const NotificationsPushbullet: React.FC = () => { ); const NotificationsPushbulletSchema = Yup.object().shape({ - accessToken: Yup.string().required( - intl.formatMessage(messages.validationAccessTokenRequired) - ), + accessToken: Yup.string().when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationAccessTokenRequired)), + otherwise: Yup.string().nullable(), + }), }); if (!data && !error) { @@ -138,28 +141,10 @@ const NotificationsPushbullet: React.FC = () => { )}
-
-
- - {intl.formatMessage(messages.notificationTypes)} - * - -
-
- - setFieldValue('types', newTypes) - } - /> -
-
-
-
+ setFieldValue('types', newTypes)} + />
diff --git a/src/components/Settings/Notifications/NotificationsPushover/index.tsx b/src/components/Settings/Notifications/NotificationsPushover/index.tsx index 8b07b900c..f7fb621e7 100644 --- a/src/components/Settings/Notifications/NotificationsPushover/index.tsx +++ b/src/components/Settings/Notifications/NotificationsPushover/index.tsx @@ -14,16 +14,15 @@ import NotificationTypeSelector from '../../../NotificationTypeSelector'; const messages = defineMessages({ agentenabled: 'Enable Agent', accessToken: 'Application/API Token', - userToken: 'User Key', + userToken: 'User or Group Key', validationAccessTokenRequired: 'You must provide a valid application token', validationUserTokenRequired: 'You must provide a valid user key', pushoversettingssaved: 'Pushover notification settings saved successfully!', pushoversettingsfailed: 'Pushover notification settings failed to save.', - testsent: 'Test notification sent!', + testsent: 'Pushover test notification sent!', settinguppushover: 'Setting Up Pushover Notifications', settinguppushoverDescription: - 'To configure Pushover notifications, you will need to register an application and enter the API token below. (You can use one of our official icons on GitHub.) You will also need your user key.', - notificationtypes: 'Notification Types', + 'To configure Pushover notifications, you will need to register an application and enter the API token below. (You can use one of the official Overseerr icons on GitHub.)', }); const NotificationsPushover: React.FC = () => { @@ -35,13 +34,25 @@ const NotificationsPushover: React.FC = () => { const NotificationsPushoverSchema = Yup.object().shape({ accessToken: Yup.string() - .required(intl.formatMessage(messages.validationAccessTokenRequired)) + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationAccessTokenRequired)), + otherwise: Yup.string().nullable(), + }) .matches( /^[a-z\d]{30}$/i, intl.formatMessage(messages.validationAccessTokenRequired) ), userToken: Yup.string() - .required(intl.formatMessage(messages.validationUserTokenRequired)) + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationUserTokenRequired)), + otherwise: Yup.string().nullable(), + }) .matches( /^[a-z\d]{30}$/i, intl.formatMessage(messages.validationUserTokenRequired) @@ -182,28 +193,10 @@ const NotificationsPushover: React.FC = () => { )}
-
-
- - {intl.formatMessage(messages.notificationtypes)} - * - -
-
- - setFieldValue('types', newTypes) - } - /> -
-
-
-
+ setFieldValue('types', newTypes)} + />
diff --git a/src/components/Settings/Notifications/NotificationsSlack/index.tsx b/src/components/Settings/Notifications/NotificationsSlack/index.tsx index 158059ce5..ced53a40a 100644 --- a/src/components/Settings/Notifications/NotificationsSlack/index.tsx +++ b/src/components/Settings/Notifications/NotificationsSlack/index.tsx @@ -16,11 +16,10 @@ const messages = defineMessages({ webhookUrl: 'Webhook URL', slacksettingssaved: 'Slack notification settings saved successfully!', slacksettingsfailed: 'Slack notification settings failed to save.', - testsent: 'Test notification sent!', + testsent: 'Slack test notification sent!', settingupslack: 'Setting Up Slack Notifications', settingupslackDescription: 'To configure Slack notifications, you will need to create an Incoming Webhook integration and enter the webhook URL below.', - notificationtypes: 'Notification Types', validationWebhookUrl: 'You must provide a valid URL', }); @@ -33,7 +32,13 @@ const NotificationsSlack: React.FC = () => { const NotificationsSlackSchema = Yup.object().shape({ webhookUrl: Yup.string() - .required(intl.formatMessage(messages.validationWebhookUrl)) + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationWebhookUrl)), + otherwise: Yup.string().nullable(), + }) .url(intl.formatMessage(messages.validationWebhookUrl)), }); @@ -136,28 +141,10 @@ const NotificationsSlack: React.FC = () => { )}
-
-
- - {intl.formatMessage(messages.notificationtypes)} - * - -
-
- - setFieldValue('types', newTypes) - } - /> -
-
-
-
+ setFieldValue('types', newTypes)} + />
diff --git a/src/components/Settings/Notifications/NotificationsTelegram.tsx b/src/components/Settings/Notifications/NotificationsTelegram.tsx index 00e8e443e..b88675133 100644 --- a/src/components/Settings/Notifications/NotificationsTelegram.tsx +++ b/src/components/Settings/Notifications/NotificationsTelegram.tsx @@ -14,17 +14,18 @@ import NotificationTypeSelector from '../../NotificationTypeSelector'; const messages = defineMessages({ agentenabled: 'Enable Agent', botUsername: 'Bot Username', + botUsernameTip: + 'Allow users to start a chat with the bot and configure their own personal notifications', botAPI: 'Bot Authentication Token', chatId: 'Chat ID', validationBotAPIRequired: 'You must provide a bot authentication token', validationChatIdRequired: 'You must provide a valid chat ID', telegramsettingssaved: 'Telegram notification settings saved successfully!', telegramsettingsfailed: 'Telegram notification settings failed to save.', - testsent: 'Test notification sent!', + testsent: 'Telegram test notification sent!', settinguptelegram: 'Setting Up Telegram Notifications', settinguptelegramDescription: 'To configure Telegram notifications, you will need to create a bot and get the bot API key. Additionally, you will need the chat ID for the chat to which you would like to send notifications. You can find this by adding @get_id_bot to the chat and issuing the /my_id command.', - notificationtypes: 'Notification Types', sendSilently: 'Send Silently', sendSilentlyTip: 'Send notifications with no sound', }); @@ -37,13 +38,23 @@ const NotificationsTelegram: React.FC = () => { ); const NotificationsTelegramSchema = Yup.object().shape({ - botAPI: Yup.string().required( - intl.formatMessage(messages.validationBotAPIRequired) - ), + botAPI: Yup.string().when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationBotAPIRequired)), + otherwise: Yup.string().nullable(), + }), chatId: Yup.string() - .required(intl.formatMessage(messages.validationChatIdRequired)) + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationChatIdRequired)), + otherwise: Yup.string().nullable(), + }) .matches( - /^[-]?\d+$/, + /^-?\d+$/, intl.formatMessage(messages.validationChatIdRequired) ), }); @@ -75,6 +86,7 @@ const NotificationsTelegram: React.FC = () => { botUsername: values.botUsername, }, }); + addToast(intl.formatMessage(messages.telegramsettingssaved), { appearance: 'success', autoDismiss: true, @@ -156,6 +168,9 @@ const NotificationsTelegram: React.FC = () => {
@@ -224,28 +239,10 @@ const NotificationsTelegram: React.FC = () => { />
-
-
- - {intl.formatMessage(messages.notificationtypes)} - * - -
-
- - setFieldValue('types', newTypes) - } - /> -
-
-
-
+ setFieldValue('types', newTypes)} + />
diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx index a2da3cbf8..4f339fff9 100644 --- a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx +++ b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx @@ -45,8 +45,7 @@ const messages = defineMessages({ validationJsonPayloadRequired: 'You must provide a valid JSON payload', webhooksettingssaved: 'Webhook notification settings saved successfully!', webhooksettingsfailed: 'Webhook notification settings failed to save.', - testsent: 'Test notification sent!', - notificationtypes: 'Notification Types', + testsent: 'Webhook test notification sent!', resetPayload: 'Reset to Default', resetPayloadSuccess: 'JSON payload reset successfully!', customJson: 'JSON Payload', @@ -63,14 +62,26 @@ const NotificationsWebhook: React.FC = () => { const NotificationsWebhookSchema = Yup.object().shape({ webhookUrl: Yup.string() - .required(intl.formatMessage(messages.validationWebhookUrl)) + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationWebhookUrl)), + otherwise: Yup.string().nullable(), + }) .matches( // eslint-disable-next-line /^(https?:)?\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*)?([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i, intl.formatMessage(messages.validationWebhookUrl) ), jsonPayload: Yup.string() - .required(intl.formatMessage(messages.validationJsonPayloadRequired)) + .when('enabled', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationJsonPayloadRequired)), + otherwise: Yup.string().nullable(), + }) .test( 'validate-json', intl.formatMessage(messages.validationJsonPayloadRequired), @@ -258,32 +269,10 @@ const NotificationsWebhook: React.FC = () => {
-
-
-
-
-
- {intl.formatMessage(messages.notificationtypes)} - * -
-
-
-
- - setFieldValue('types', newTypes) - } - /> -
-
-
-
-
+ setFieldValue('types', newTypes)} + />
diff --git a/src/components/Settings/SettingsLayout.tsx b/src/components/Settings/SettingsLayout.tsx index dd13cfe44..65f4d5485 100644 --- a/src/components/Settings/SettingsLayout.tsx +++ b/src/components/Settings/SettingsLayout.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; import { defineMessages, useIntl } from 'react-intl'; -import PageTitle from '../Common/PageTitle'; import globalMessages from '../../i18n/globalMessages'; +import PageTitle from '../Common/PageTitle'; +import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs'; const messages = defineMessages({ menuGeneralSettings: 'General', @@ -16,14 +15,7 @@ const messages = defineMessages({ menuAbout: 'About', }); -interface SettingsRoute { - text: string; - route: string; - regex: RegExp; -} - const SettingsLayout: React.FC = ({ children }) => { - const router = useRouter(); const intl = useIntl(); const settingsRoutes: SettingsRoute[] = [ @@ -69,78 +61,11 @@ const SettingsLayout: React.FC = ({ children }) => { }, ]; - const activeLinkColor = - 'border-indigo-600 text-indigo-500 focus:outline-none focus:text-indigo-500 focus:border-indigo-500'; - - const inactiveLinkColor = - 'border-transparent text-gray-500 hover:text-gray-400 hover:border-gray-300 focus:outline-none focus:text-gray-4700 focus:border-gray-300'; - - const SettingsLink: React.FC<{ - route: string; - regex: RegExp; - isMobile?: boolean; - }> = ({ children, route, regex, isMobile = false }) => { - if (isMobile) { - return ; - } - - return ( - - - {children} - - - ); - }; return ( <>
-
- -
-
- -
+
{children}
diff --git a/src/components/Settings/SettingsNotifications.tsx b/src/components/Settings/SettingsNotifications.tsx index a6893a38f..761c73278 100644 --- a/src/components/Settings/SettingsNotifications.tsx +++ b/src/components/Settings/SettingsNotifications.tsx @@ -1,11 +1,5 @@ -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; -import Link from 'next/link'; -import { useRouter } from 'next/router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; -import useSWR from 'swr'; import Bolt from '../../assets/bolt.svg'; import DiscordLogo from '../../assets/extlogos/discord.svg'; import PushbulletLogo from '../../assets/extlogos/pushbullet.svg'; @@ -13,38 +7,22 @@ import PushoverLogo from '../../assets/extlogos/pushover.svg'; import SlackLogo from '../../assets/extlogos/slack.svg'; import TelegramLogo from '../../assets/extlogos/telegram.svg'; import globalMessages from '../../i18n/globalMessages'; -import Error from '../../pages/_error'; -import Button from '../Common/Button'; -import LoadingSpinner from '../Common/LoadingSpinner'; import PageTitle from '../Common/PageTitle'; +import SettingsTabs, { SettingsRoute } from '../Common/SettingsTabs'; const messages = defineMessages({ notifications: 'Notifications', notificationsettings: 'Notification Settings', - notificationsettingsDescription: - 'Configure global notification settings. The options below will apply to all notification agents.', - notificationAgentsSettings: 'Notification Agents', notificationAgentSettingsDescription: - 'Choose the types of notifications to send, and which notification agents to use.', + 'Configure and enable notification agents.', notificationsettingssaved: 'Notification settings saved successfully!', notificationsettingsfailed: 'Notification settings failed to save.', - enablenotifications: 'Enable Notifications', email: 'Email', webhook: 'Webhook', }); -interface SettingsRoute { - text: string; - content: React.ReactNode; - route: string; - regex: RegExp; -} - const SettingsNotifications: React.FC = ({ children }) => { - const router = useRouter(); const intl = useIntl(); - const { addToast } = useToasts(); - const { data, error, revalidate } = useSWR('/api/v1/settings/notifications'); const settingsRoutes: SettingsRoute[] = [ { @@ -139,40 +117,6 @@ const SettingsNotifications: React.FC = ({ children }) => { }, ]; - const activeLinkColor = 'bg-indigo-700'; - const inactiveLinkColor = 'bg-gray-800'; - - const SettingsLink: React.FC<{ - route: string; - regex: RegExp; - isMobile?: boolean; - }> = ({ children, route, regex, isMobile = false }) => { - if (isMobile) { - return ; - } - - return ( - - - {children} - - - ); - }; - - if (!data && !error) { - return ; - } - - if (!data) { - return ; - } - return ( <> {

{intl.formatMessage(messages.notificationsettings)}

-

- {intl.formatMessage(messages.notificationsettingsDescription)} -

-
-
- { - try { - await axios.post('/api/v1/settings/notifications', { - enabled: values.enabled, - }); - addToast(intl.formatMessage(messages.notificationsettingssaved), { - appearance: 'success', - autoDismiss: true, - }); - } catch (e) { - addToast( - intl.formatMessage(messages.notificationsettingsfailed), - { - appearance: 'error', - autoDismiss: true, - } - ); - } finally { - revalidate(); - } - }} - > - {({ isSubmitting, values, setFieldValue }) => { - return ( -
-
- -
- { - setFieldValue('enabled', !values.enabled); - }} - /> -
-
-
-
- - - -
-
-
- ); - }} -
-
-
-

- {intl.formatMessage(messages.notificationAgentsSettings)} -

{intl.formatMessage(messages.notificationAgentSettingsDescription)}

-
-
- - -
-
- -
-
+
{children}
); diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx new file mode 100644 index 000000000..244e1d0d6 --- /dev/null +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord.tsx @@ -0,0 +1,178 @@ +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; +import * as Yup from 'yup'; +import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; +import { + hasNotificationAgentEnabled, + NotificationAgentType, +} from '../../../../../server/lib/notifications/agenttypes'; +import { useUser } from '../../../../hooks/useUser'; +import globalMessages from '../../../../i18n/globalMessages'; +import Button from '../../../Common/Button'; +import LoadingSpinner from '../../../Common/LoadingSpinner'; + +const messages = defineMessages({ + discordsettingssaved: 'Discord notification settings saved successfully!', + discordsettingsfailed: 'Discord notification settings failed to save.', + enableDiscord: 'Enable Mentions', + discordId: 'User ID', + discordIdTip: + 'The ID number for your user account', + validationDiscordId: 'You must provide a valid user ID', +}); + +const UserNotificationsDiscord: React.FC = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const router = useRouter(); + const [notificationAgents, setNotificationAgents] = useState(0); + const { user } = useUser({ id: Number(router.query.userId) }); + const { data, error, revalidate } = useSWR( + user ? `/api/v1/user/${user?.id}/settings/notifications` : null + ); + + useEffect(() => { + setNotificationAgents( + data?.notificationAgents ?? NotificationAgentType.EMAIL + ); + }, [data]); + + const UserNotificationsDiscordSchema = Yup.object().shape({ + discordId: Yup.string() + .when('enableDiscord', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationDiscordId)), + otherwise: Yup.string().nullable(), + }) + .matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)), + }); + + if (!data && !error) { + return ; + } + + return ( + { + try { + await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { + notificationAgents, + pgpKey: data?.pgpKey, + discordId: values.discordId, + telegramChatId: data?.telegramChatId, + telegramSendSilently: data?.telegramSendSilently, + }); + addToast(intl.formatMessage(messages.discordsettingssaved), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.discordsettingsfailed), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + revalidate(); + } + }} + > + {({ errors, touched, isSubmitting, isValid, values, setFieldValue }) => { + return ( +
+ {data?.discordEnabled && ( +
+ +
+ { + setNotificationAgents( + hasNotificationAgentEnabled( + NotificationAgentType.DISCORD, + notificationAgents + ) + ? notificationAgents - NotificationAgentType.DISCORD + : notificationAgents + NotificationAgentType.DISCORD + ); + setFieldValue('enableDiscord', !values.enableDiscord); + }} + /> +
+
+ )} +
+ +
+
+ +
+ {errors.discordId && touched.discordId && ( +
{errors.discordId}
+ )} +
+
+
+
+ + + +
+
+
+ ); + }} +
+ ); +}; + +export default UserNotificationsDiscord; diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx new file mode 100644 index 000000000..b949fb95a --- /dev/null +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail.tsx @@ -0,0 +1,175 @@ +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; +import * as Yup from 'yup'; +import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; +import { + hasNotificationAgentEnabled, + NotificationAgentType, +} from '../../../../../server/lib/notifications/agenttypes'; +import { useUser } from '../../../../hooks/useUser'; +import globalMessages from '../../../../i18n/globalMessages'; +import Badge from '../../../Common/Badge'; +import Button from '../../../Common/Button'; +import LoadingSpinner from '../../../Common/LoadingSpinner'; +import { OpenPgpLink } from '../../../Settings/Notifications/NotificationsEmail'; + +const messages = defineMessages({ + emailsettingssaved: 'Email notification settings saved successfully!', + emailsettingsfailed: 'Email notification settings failed to save.', + enableEmail: 'Enable Notifications', + pgpPublicKey: 'PGP Public Key', + pgpPublicKeyTip: + 'Encrypt email messages using OpenPGP', + validationPgpPublicKey: 'You must provide a valid PGP public key', +}); + +const UserEmailSettings: React.FC = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const router = useRouter(); + const [notificationAgents, setNotificationAgents] = useState(0); + const { user } = useUser({ id: Number(router.query.userId) }); + const { data, error, revalidate } = useSWR( + user ? `/api/v1/user/${user?.id}/settings/notifications` : null + ); + + useEffect(() => { + setNotificationAgents( + data?.notificationAgents ?? NotificationAgentType.EMAIL + ); + }, [data]); + + const UserNotificationsEmailSchema = Yup.object().shape({ + pgpKey: Yup.string() + .nullable() + .matches( + /^-----BEGIN PGP PUBLIC KEY BLOCK-----.+-----END PGP PUBLIC KEY BLOCK-----$/, + intl.formatMessage(messages.validationPgpPublicKey) + ), + }); + + if (!data && !error) { + return ; + } + + return ( + { + try { + await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { + notificationAgents, + pgpKey: values.pgpKey, + discordId: data?.discordId, + telegramChatId: data?.telegramChatId, + telegramSendSilently: data?.telegramSendSilently, + }); + addToast(intl.formatMessage(messages.emailsettingssaved), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.emailsettingsfailed), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + revalidate(); + } + }} + > + {({ errors, touched, isSubmitting, isValid, values, setFieldValue }) => { + return ( +
+
+ +
+ { + setNotificationAgents( + hasNotificationAgentEnabled( + NotificationAgentType.EMAIL, + notificationAgents + ) + ? notificationAgents - NotificationAgentType.EMAIL + : notificationAgents + NotificationAgentType.EMAIL + ); + setFieldValue('enableEmail', !values.enableEmail); + }} + /> +
+
+
+ +
+
+ +
+ {errors.pgpKey && touched.pgpKey && ( +
{errors.pgpKey}
+ )} +
+
+
+
+ + + +
+
+
+ ); + }} +
+ ); +}; + +export default UserEmailSettings; diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx new file mode 100644 index 000000000..6193e127f --- /dev/null +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram.tsx @@ -0,0 +1,217 @@ +import axios from 'axios'; +import { Field, Form, Formik } from 'formik'; +import { useRouter } from 'next/router'; +import React, { useEffect, useState } from 'react'; +import { defineMessages, useIntl } from 'react-intl'; +import { useToasts } from 'react-toast-notifications'; +import useSWR from 'swr'; +import * as Yup from 'yup'; +import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; +import { + hasNotificationAgentEnabled, + NotificationAgentType, +} from '../../../../../server/lib/notifications/agenttypes'; +import { useUser } from '../../../../hooks/useUser'; +import globalMessages from '../../../../i18n/globalMessages'; +import Button from '../../../Common/Button'; +import LoadingSpinner from '../../../Common/LoadingSpinner'; + +const messages = defineMessages({ + telegramsettingssaved: 'Telegram notification settings saved successfully!', + telegramsettingsfailed: 'Telegram notification settings failed to save.', + enableTelegram: 'Enable Notifications', + telegramChatId: 'Chat ID', + telegramChatIdTipLong: + 'Start a chat, add @get_id_bot, and issue the /my_id command', + sendSilently: 'Send Silently', + sendSilentlyDescription: 'Send notifications with no sound', + validationTelegramChatId: 'You must provide a valid chat ID', +}); + +const UserTelegramSettings: React.FC = () => { + const intl = useIntl(); + const { addToast } = useToasts(); + const router = useRouter(); + const [notificationAgents, setNotificationAgents] = useState(0); + const { user } = useUser({ id: Number(router.query.userId) }); + const { data, error, revalidate } = useSWR( + user ? `/api/v1/user/${user?.id}/settings/notifications` : null + ); + + useEffect(() => { + setNotificationAgents( + data?.notificationAgents ?? NotificationAgentType.EMAIL + ); + }, [data]); + + const UserNotificationsTelegramSchema = Yup.object().shape({ + telegramChatId: Yup.string() + .when('enableTelegram', { + is: true, + then: Yup.string() + .nullable() + .required(intl.formatMessage(messages.validationTelegramChatId)), + otherwise: Yup.string().nullable(), + }) + .matches( + /^-?\d+$/, + intl.formatMessage(messages.validationTelegramChatId) + ), + }); + + if (!data && !error) { + return ; + } + + return ( + { + try { + await axios.post(`/api/v1/user/${user?.id}/settings/notifications`, { + notificationAgents, + pgpKey: data?.pgpKey, + discordId: data?.discordId, + telegramChatId: values.telegramChatId, + telegramSendSilently: values.telegramSendSilently, + }); + addToast(intl.formatMessage(messages.telegramsettingssaved), { + appearance: 'success', + autoDismiss: true, + }); + } catch (e) { + addToast(intl.formatMessage(messages.telegramsettingsfailed), { + appearance: 'error', + autoDismiss: true, + }); + } finally { + revalidate(); + } + }} + > + {({ errors, touched, isSubmitting, isValid, values, setFieldValue }) => { + return ( +
+
+ +
+ { + setNotificationAgents( + hasNotificationAgentEnabled( + NotificationAgentType.TELEGRAM, + notificationAgents + ) + ? notificationAgents - NotificationAgentType.TELEGRAM + : notificationAgents + NotificationAgentType.TELEGRAM + ); + setFieldValue('enableTelegram', !values.enableTelegram); + }} + /> +
+
+
+ +
+
+ +
+ {errors.telegramChatId && touched.telegramChatId && ( +
{errors.telegramChatId}
+ )} +
+
+
+ +
+ +
+
+
+
+ + + +
+
+
+ ); + }} +
+ ); +}; + +export default UserTelegramSettings; diff --git a/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx b/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx index df828e5b0..b52db4813 100644 --- a/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserNotificationSettings/index.tsx @@ -1,61 +1,88 @@ -import axios from 'axios'; -import { Field, Form, Formik } from 'formik'; import { useRouter } from 'next/router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; -import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; -import * as Yup from 'yup'; import { UserSettingsNotificationsResponse } from '../../../../../server/interfaces/api/userSettingsInterfaces'; +import DiscordLogo from '../../../../assets/extlogos/discord.svg'; +import TelegramLogo from '../../../../assets/extlogos/telegram.svg'; import { useUser } from '../../../../hooks/useUser'; import globalMessages from '../../../../i18n/globalMessages'; import Error from '../../../../pages/_error'; -import Badge from '../../../Common/Badge'; -import Button from '../../../Common/Button'; import LoadingSpinner from '../../../Common/LoadingSpinner'; import PageTitle from '../../../Common/PageTitle'; -import { PgpLink } from '../../../Settings/Notifications/NotificationsEmail'; +import SettingsTabs, { SettingsRoute } from '../../../Common/SettingsTabs'; const messages = defineMessages({ notifications: 'Notifications', notificationsettings: 'Notification Settings', - enableNotifications: 'Enable Notifications', - discordId: 'Discord ID', - discordIdTip: - 'The ID number for your Discord user account', - validationDiscordId: 'You must provide a valid Discord user ID', - telegramChatId: 'Telegram Chat ID', - telegramChatIdTip: 'Add @get_id_bot to the chat', - telegramChatIdTipLong: - 'Start a chat, add @get_id_bot, and issue the /my_id command', - sendSilently: 'Send Telegram Messages Silently', - sendSilentlyDescription: 'Send notifications with no sound', - validationTelegramChatId: 'You must provide a valid Telegram chat ID', + email: 'Email', toastSettingsSuccess: 'Notification settings saved successfully!', toastSettingsFailure: 'Something went wrong while saving settings.', - pgpKey: 'PGP Public Key', - pgpKeyTip: 'Encrypt email messages', }); -const UserNotificationSettings: React.FC = () => { +const UserNotificationSettings: React.FC = ({ children }) => { const intl = useIntl(); - const { addToast } = useToasts(); const router = useRouter(); - const { user, mutate } = useUser({ id: Number(router.query.userId) }); - const { data, error, revalidate } = useSWR( + const { user } = useUser({ id: Number(router.query.userId) }); + const { data, error } = useSWR( user ? `/api/v1/user/${user?.id}/settings/notifications` : null ); - const UserNotificationSettingsSchema = Yup.object().shape({ - discordId: Yup.string() - .nullable() - .matches(/^\d{17,18}$/, intl.formatMessage(messages.validationDiscordId)), - telegramChatId: Yup.string() - .nullable() - .matches( - /^[-]?\d+$/, - intl.formatMessage(messages.validationTelegramChatId) + const settingsRoutes: SettingsRoute[] = [ + { + text: intl.formatMessage(messages.email), + content: ( + + + + + {intl.formatMessage(messages.email)} + ), + route: '/settings/notifications/email', + regex: /\/settings\/notifications\/email/, + hidden: !data?.emailEnabled, + }, + { + text: 'Discord', + content: ( + + + Discord + + ), + route: '/settings/notifications/discord', + regex: /\/settings\/notifications\/discord/, + }, + { + text: 'Telegram', + content: ( + + + Telegram + + ), + route: '/settings/notifications/telegram', + regex: /\/settings\/notifications\/telegram/, + hidden: !data?.telegramEnabled || !data?.telegramBotUsername, + }, + ]; + + settingsRoutes.forEach((settingsRoute) => { + settingsRoute.route = router.asPath.includes('/profile') + ? `/profile${settingsRoute.route}` + : `/users/${user?.id}${settingsRoute.route}`; }); if (!data && !error) { @@ -80,215 +107,8 @@ const UserNotificationSettings: React.FC = () => { {intl.formatMessage(messages.notificationsettings)}
- { - try { - await axios.post( - `/api/v1/user/${user?.id}/settings/notifications`, - { - enableNotifications: values.enableNotifications, - discordId: values.discordId, - telegramChatId: values.telegramChatId, - telegramSendSilently: values.telegramSendSilently, - pgpKey: values.pgpKey, - } - ); - - addToast(intl.formatMessage(messages.toastSettingsSuccess), { - autoDismiss: true, - appearance: 'success', - }); - } catch (e) { - addToast(intl.formatMessage(messages.toastSettingsFailure), { - autoDismiss: true, - appearance: 'error', - }); - } finally { - revalidate(); - mutate(); - } - }} - > - {({ errors, touched, isSubmitting }) => { - return ( -
-
- -
- -
-
-
- -
-
- -
- {errors.pgpKey && touched.pgpKey && ( -
{errors.pgpKey}
- )} -
-
-
- -
-
- -
- {errors.discordId && touched.discordId && ( -
{errors.discordId}
- )} -
-
-
- -
-
- -
- {errors.telegramChatId && touched.telegramChatId && ( -
{errors.telegramChatId}
- )} -
-
-
- -
- -
-
-
-
- - - -
-
-
- ); - }} -
+ +
{children}
); }; diff --git a/src/components/UserProfile/UserSettings/index.tsx b/src/components/UserProfile/UserSettings/index.tsx index d6babb63d..8863495f5 100644 --- a/src/components/UserProfile/UserSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/index.tsx @@ -1,7 +1,8 @@ -import Link from 'next/link'; import { useRouter } from 'next/router'; import React from 'react'; import { defineMessages, useIntl } from 'react-intl'; +import useSWR from 'swr'; +import { UserSettingsNotificationsResponse } from '../../../../server/interfaces/api/userSettingsInterfaces'; import { hasPermission, Permission } from '../../../../server/lib/permissions'; import useSettings from '../../../hooks/useSettings'; import { useUser } from '../../../hooks/useUser'; @@ -10,6 +11,7 @@ import Error from '../../../pages/_error'; import Alert from '../../Common/Alert'; import LoadingSpinner from '../../Common/LoadingSpinner'; import PageTitle from '../../Common/PageTitle'; +import SettingsTabs, { SettingsRoute } from '../../Common/SettingsTabs'; import ProfileHeader from '../ProfileHeader'; const messages = defineMessages({ @@ -21,21 +23,15 @@ const messages = defineMessages({ "You do not have permission to modify this user's settings.", }); -interface SettingsRoute { - text: string; - route: string; - regex: RegExp; - requiredPermission?: Permission | Permission[]; - permissionType?: { type: 'and' | 'or' }; - hidden?: boolean; -} - const UserSettings: React.FC = ({ children }) => { const router = useRouter(); const settings = useSettings(); const { user: currentUser } = useUser(); const { user, error } = useUser({ id: Number(router.query.userId) }); const intl = useIntl(); + const { data } = useSWR( + user ? `/api/v1/user/${user?.id}/settings/notifications` : null + ); if (!user && !error) { return ; @@ -67,7 +63,9 @@ const UserSettings: React.FC = ({ children }) => { }, { text: intl.formatMessage(messages.menuNotifications), - route: '/settings/notifications', + route: data?.emailEnabled + ? '/settings/notifications/email' + : '/settings/notifications/discord', regex: /\/settings\/notifications/, }, { @@ -79,38 +77,6 @@ const UserSettings: React.FC = ({ children }) => { }, ]; - const activeLinkColor = - 'border-indigo-600 text-indigo-500 focus:outline-none focus:text-indigo-500 focus:border-indigo-500'; - - const inactiveLinkColor = - 'border-transparent text-gray-500 hover:text-gray-400 hover:border-gray-300 focus:outline-none focus:text-gray-4700 focus:border-gray-300'; - - const SettingsLink: React.FC<{ - route: string; - regex: RegExp; - isMobile?: boolean; - }> = ({ children, route, regex, isMobile = false }) => { - const finalRoute = router.asPath.includes('/profile') - ? `/profile${route}` - : `/users/${user.id}${route}`; - if (isMobile) { - return ; - } - - return ( - - - {children} - - - ); - }; - if (currentUser?.id !== 1 && user.id === 1) { return ( <> @@ -133,13 +99,11 @@ const UserSettings: React.FC = ({ children }) => { ); } - const currentRoute = settingsRoutes.find( - (route) => !!router.pathname.match(route.regex) - )?.route; - - const finalRoute = router.asPath.includes('/profile') - ? `/profile${currentRoute}` - : `/users/${user.id}${currentRoute}`; + settingsRoutes.forEach((settingsRoute) => { + settingsRoute.route = router.asPath.includes('/profile') + ? `/profile${settingsRoute.route}` + : `/users/${user.id}${settingsRoute.route}`; + }); return ( <> @@ -151,68 +115,7 @@ const UserSettings: React.FC = ({ children }) => { />
-
- -
-
-
- -
-
+
{children}
diff --git a/src/context/SettingsContext.tsx b/src/context/SettingsContext.tsx index 8e83a4c97..8c9033f0f 100644 --- a/src/context/SettingsContext.tsx +++ b/src/context/SettingsContext.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces'; import useSWR from 'swr'; +import { PublicSettingsResponse } from '../../server/interfaces/api/settingsInterfaces'; export interface SettingsContextProps { currentSettings: PublicSettingsResponse; diff --git a/src/hooks/useUser.ts b/src/hooks/useUser.ts index 2e737d55f..867303f12 100644 --- a/src/hooks/useUser.ts +++ b/src/hooks/useUser.ts @@ -26,7 +26,6 @@ export interface User { } export interface UserSettings { - enableNotifications: boolean; discordId?: string; region?: string; originalLanguage?: string; diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index f0d67ffbc..dfa062628 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -96,6 +96,7 @@ "components.NotificationTypeSelector.mediafailedDescription": "Sends a notification when requested media fails to be added to Radarr or Sonarr.", "components.NotificationTypeSelector.mediarequested": "Media Requested", "components.NotificationTypeSelector.mediarequestedDescription": "Sends a notification when media is requested and requires approval.", + "components.NotificationTypeSelector.notificationTypes": "Notification Types", "components.PermissionEdit.admin": "Admin", "components.PermissionEdit.adminDescription": "Full administrator access. Bypasses all other permission checks.", "components.PermissionEdit.advancedrequest": "Advanced Requests", @@ -243,41 +244,37 @@ "components.Search.searchresults": "Search Results", "components.Settings.Notifications.NotificationsPushbullet.accessToken": "Access Token", "components.Settings.Notifications.NotificationsPushbullet.agentEnabled": "Enable Agent", - "components.Settings.Notifications.NotificationsPushbullet.notificationTypes": "Notification Types", "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsFailed": "Pushbullet notification settings failed to save.", "components.Settings.Notifications.NotificationsPushbullet.pushbulletSettingsSaved": "Pushbullet notification settings saved successfully!", "components.Settings.Notifications.NotificationsPushbullet.settingUpPushbullet": "Setting Up Pushbullet Notifications", - "components.Settings.Notifications.NotificationsPushbullet.settingUpPushbulletDescription": "To configure Pushbullet notifications, you will need to create an access token and enter it below.", - "components.Settings.Notifications.NotificationsPushbullet.testSent": "Test notification sent!", + "components.Settings.Notifications.NotificationsPushbullet.settingUpPushbulletDescription": "To configure Pushbullet notifications, you will need to create an access token.", + "components.Settings.Notifications.NotificationsPushbullet.testSent": "Pushbullet test notification sent!", "components.Settings.Notifications.NotificationsPushbullet.validationAccessTokenRequired": "You must provide an access token", "components.Settings.Notifications.NotificationsPushover.accessToken": "Application/API Token", "components.Settings.Notifications.NotificationsPushover.agentenabled": "Enable Agent", - "components.Settings.Notifications.NotificationsPushover.notificationtypes": "Notification Types", "components.Settings.Notifications.NotificationsPushover.pushoversettingsfailed": "Pushover notification settings failed to save.", "components.Settings.Notifications.NotificationsPushover.pushoversettingssaved": "Pushover notification settings saved successfully!", "components.Settings.Notifications.NotificationsPushover.settinguppushover": "Setting Up Pushover Notifications", - "components.Settings.Notifications.NotificationsPushover.settinguppushoverDescription": "To configure Pushover notifications, you will need to register an application and enter the API token below. (You can use one of our official icons on GitHub.) You will also need your user key.", - "components.Settings.Notifications.NotificationsPushover.testsent": "Test notification sent!", - "components.Settings.Notifications.NotificationsPushover.userToken": "User Key", + "components.Settings.Notifications.NotificationsPushover.settinguppushoverDescription": "To configure Pushover notifications, you will need to register an application and enter the API token below. (You can use one of the official Overseerr icons on GitHub.)", + "components.Settings.Notifications.NotificationsPushover.testsent": "Pushover test notification sent!", + "components.Settings.Notifications.NotificationsPushover.userToken": "User or Group Key", "components.Settings.Notifications.NotificationsPushover.validationAccessTokenRequired": "You must provide a valid application token", "components.Settings.Notifications.NotificationsPushover.validationUserTokenRequired": "You must provide a valid user key", "components.Settings.Notifications.NotificationsSlack.agentenabled": "Enable Agent", - "components.Settings.Notifications.NotificationsSlack.notificationtypes": "Notification Types", "components.Settings.Notifications.NotificationsSlack.settingupslack": "Setting Up Slack Notifications", "components.Settings.Notifications.NotificationsSlack.settingupslackDescription": "To configure Slack notifications, you will need to create an Incoming Webhook integration and enter the webhook URL below.", "components.Settings.Notifications.NotificationsSlack.slacksettingsfailed": "Slack notification settings failed to save.", "components.Settings.Notifications.NotificationsSlack.slacksettingssaved": "Slack notification settings saved successfully!", - "components.Settings.Notifications.NotificationsSlack.testsent": "Test notification sent!", + "components.Settings.Notifications.NotificationsSlack.testsent": "Slack test notification sent!", "components.Settings.Notifications.NotificationsSlack.validationWebhookUrl": "You must provide a valid URL", "components.Settings.Notifications.NotificationsSlack.webhookUrl": "Webhook URL", "components.Settings.Notifications.NotificationsWebhook.agentenabled": "Enable Agent", "components.Settings.Notifications.NotificationsWebhook.authheader": "Authorization Header", "components.Settings.Notifications.NotificationsWebhook.customJson": "JSON Payload", - "components.Settings.Notifications.NotificationsWebhook.notificationtypes": "Notification Types", "components.Settings.Notifications.NotificationsWebhook.resetPayload": "Reset to Default", "components.Settings.Notifications.NotificationsWebhook.resetPayloadSuccess": "JSON payload reset successfully!", "components.Settings.Notifications.NotificationsWebhook.templatevariablehelp": "Template Variable Help", - "components.Settings.Notifications.NotificationsWebhook.testsent": "Test notification sent!", + "components.Settings.Notifications.NotificationsWebhook.testsent": "Webhook test notification sent!", "components.Settings.Notifications.NotificationsWebhook.validationJsonPayloadRequired": "You must provide a valid JSON payload", "components.Settings.Notifications.NotificationsWebhook.validationWebhookUrl": "You must provide a valid URL", "components.Settings.Notifications.NotificationsWebhook.webhookUrl": "Webhook URL", @@ -290,6 +287,7 @@ "components.Settings.Notifications.botAPI": "Bot Authentication Token", "components.Settings.Notifications.botAvatarUrl": "Bot Avatar URL", "components.Settings.Notifications.botUsername": "Bot Username", + "components.Settings.Notifications.botUsernameTip": "Allow users to start a chat with the bot and configure their own personal notifications", "components.Settings.Notifications.chatId": "Chat ID", "components.Settings.Notifications.discordsettingsfailed": "Discord notification settings failed to save.", "components.Settings.Notifications.discordsettingssaved": "Discord notification settings saved successfully!", @@ -300,11 +298,10 @@ "components.Settings.Notifications.emailsettingsfailed": "Email notification settings failed to save.", "components.Settings.Notifications.emailsettingssaved": "Email notification settings saved successfully!", "components.Settings.Notifications.enableSsl": "Enable SSL", - "components.Settings.Notifications.notificationtypes": "Notification Types", - "components.Settings.Notifications.pgpPassword": "PGP Password", - "components.Settings.Notifications.pgpPasswordTip": "Sign encrypted email messages (PGP private key is also required)", - "components.Settings.Notifications.pgpPrivateKey": "PGP Private Key", - "components.Settings.Notifications.pgpPrivateKeyTip": "Sign encrypted email messages (PGP password is also required)", + "components.Settings.Notifications.pgpPassword": "PGP Password", + "components.Settings.Notifications.pgpPasswordTip": "Sign encrypted email messages using OpenPGP", + "components.Settings.Notifications.pgpPrivateKey": "PGP Private Key", + "components.Settings.Notifications.pgpPrivateKeyTip": "Sign encrypted email messages using OpenPGP", "components.Settings.Notifications.sendSilently": "Send Silently", "components.Settings.Notifications.sendSilentlyTip": "Send notifications with no sound", "components.Settings.Notifications.senderName": "Sender Name", @@ -315,11 +312,13 @@ "components.Settings.Notifications.ssldisabletip": "SSL should be disabled on standard TLS connections (port 587)", "components.Settings.Notifications.telegramsettingsfailed": "Telegram notification settings failed to save.", "components.Settings.Notifications.telegramsettingssaved": "Telegram notification settings saved successfully!", - "components.Settings.Notifications.testsent": "Test notification sent!", + "components.Settings.Notifications.testsent": "Telegram test notification sent!", "components.Settings.Notifications.validationBotAPIRequired": "You must provide a bot authentication token", "components.Settings.Notifications.validationChatIdRequired": "You must provide a valid chat ID", "components.Settings.Notifications.validationEmail": "You must provide a valid email address", - "components.Settings.Notifications.validationSmtpHostRequired": "You must provide a hostname or IP address", + "components.Settings.Notifications.validationPgpPassword": "You must provide a PGP password if a PGP private key is entered", + "components.Settings.Notifications.validationPgpPrivateKey": "You must provide a valid PGP private key if a PGP password is entered", + "components.Settings.Notifications.validationSmtpHostRequired": "You must provide a valid hostname or IP address", "components.Settings.Notifications.validationSmtpPortRequired": "You must provide a valid port number", "components.Settings.Notifications.validationUrl": "You must provide a valid URL", "components.Settings.Notifications.webhookUrl": "Webhook URL", @@ -524,7 +523,6 @@ "components.Settings.default4k": "Default 4K", "components.Settings.deleteserverconfirm": "Are you sure you want to delete this server?", "components.Settings.email": "Email", - "components.Settings.enablenotifications": "Enable Notifications", "components.Settings.enablessl": "Enable SSL", "components.Settings.general": "General", "components.Settings.generalsettings": "General Settings", @@ -544,11 +542,9 @@ "components.Settings.menuUsers": "Users", "components.Settings.nodefault": "No Default Server", "components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.", - "components.Settings.notificationAgentSettingsDescription": "Choose the types of notifications to send, and which notification agents to use.", - "components.Settings.notificationAgentsSettings": "Notification Agents", + "components.Settings.notificationAgentSettingsDescription": "Configure and enable notification agents.", "components.Settings.notifications": "Notifications", "components.Settings.notificationsettings": "Notification Settings", - "components.Settings.notificationsettingsDescription": "Configure global notification settings. The options below will apply to all notification agents.", "components.Settings.notificationsettingsfailed": "Notification settings failed to save.", "components.Settings.notificationsettingssaved": "Notification settings saved successfully!", "components.Settings.notrunning": "Not Running", @@ -715,22 +711,31 @@ "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsFailure": "Something went wrong while saving settings.", "components.UserProfile.UserSettings.UserGeneralSettings.toastSettingsSuccess": "Settings saved successfully!", "components.UserProfile.UserSettings.UserGeneralSettings.user": "User", - "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "Discord ID", - "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The ID number for your Discord user account", - "components.UserProfile.UserSettings.UserNotificationSettings.enableNotifications": "Enable Notifications", + "components.UserProfile.UserSettings.UserNotificationSettings.discordId": "User ID", + "components.UserProfile.UserSettings.UserNotificationSettings.discordIdTip": "The ID number for your user account", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingsfailed": "Discord notification settings failed to save.", + "components.UserProfile.UserSettings.UserNotificationSettings.discordsettingssaved": "Discord notification settings saved successfully!", + "components.UserProfile.UserSettings.UserNotificationSettings.email": "Email", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingsfailed": "Email notification settings failed to save.", + "components.UserProfile.UserSettings.UserNotificationSettings.emailsettingssaved": "Email notification settings saved successfully!", + "components.UserProfile.UserSettings.UserNotificationSettings.enableDiscord": "Enable Mentions", + "components.UserProfile.UserSettings.UserNotificationSettings.enableEmail": "Enable Notifications", + "components.UserProfile.UserSettings.UserNotificationSettings.enableTelegram": "Enable Notifications", "components.UserProfile.UserSettings.UserNotificationSettings.notifications": "Notifications", "components.UserProfile.UserSettings.UserNotificationSettings.notificationsettings": "Notification Settings", - "components.UserProfile.UserSettings.UserNotificationSettings.pgpKey": "PGP Public Key", - "components.UserProfile.UserSettings.UserNotificationSettings.pgpKeyTip": "Encrypt email messages", - "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Send Telegram Messages Silently", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKey": "PGP Public Key", + "components.UserProfile.UserSettings.UserNotificationSettings.pgpPublicKeyTip": "Encrypt email messages using OpenPGP", + "components.UserProfile.UserSettings.UserNotificationSettings.sendSilently": "Send Silently", "components.UserProfile.UserSettings.UserNotificationSettings.sendSilentlyDescription": "Send notifications with no sound", - "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Telegram Chat ID", - "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTip": "Add @get_id_bot to the chat", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatId": "Chat ID", "components.UserProfile.UserSettings.UserNotificationSettings.telegramChatIdTipLong": "Start a chat, add @get_id_bot, and issue the /my_id command", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingsfailed": "Telegram notification settings failed to save.", + "components.UserProfile.UserSettings.UserNotificationSettings.telegramsettingssaved": "Telegram notification settings saved successfully!", "components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsFailure": "Something went wrong while saving settings.", "components.UserProfile.UserSettings.UserNotificationSettings.toastSettingsSuccess": "Notification settings saved successfully!", - "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid Discord user ID", - "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid Telegram chat ID", + "components.UserProfile.UserSettings.UserNotificationSettings.validationDiscordId": "You must provide a valid user ID", + "components.UserProfile.UserSettings.UserNotificationSettings.validationPgpPublicKey": "You must provide a valid PGP public key", + "components.UserProfile.UserSettings.UserNotificationSettings.validationTelegramChatId": "You must provide a valid chat ID", "components.UserProfile.UserSettings.UserPasswordChange.confirmpassword": "Confirm Password", "components.UserProfile.UserSettings.UserPasswordChange.currentpassword": "Current Password", "components.UserProfile.UserSettings.UserPasswordChange.newpassword": "New Password", diff --git a/src/pages/profile/settings/notifications.tsx b/src/pages/profile/settings/notifications.tsx deleted file mode 100644 index dcb27361b..000000000 --- a/src/pages/profile/settings/notifications.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { NextPage } from 'next'; -import React from 'react'; -import UserSettings from '../../../components/UserProfile/UserSettings'; -import UserNotificationSettings from '../../../components/UserProfile/UserSettings/UserNotificationSettings'; - -const UserSettingsMainPage: NextPage = () => { - return ( - - - - ); -}; - -export default UserSettingsMainPage; diff --git a/src/pages/profile/settings/notifications/discord.tsx b/src/pages/profile/settings/notifications/discord.tsx new file mode 100644 index 000000000..06e580ffd --- /dev/null +++ b/src/pages/profile/settings/notifications/discord.tsx @@ -0,0 +1,17 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsDiscord from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord'; + +const NotificationsPage: NextPage = () => { + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/pages/profile/settings/notifications/email.tsx b/src/pages/profile/settings/notifications/email.tsx new file mode 100644 index 000000000..370258cac --- /dev/null +++ b/src/pages/profile/settings/notifications/email.tsx @@ -0,0 +1,17 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsEmail from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail'; + +const NotificationsPage: NextPage = () => { + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/pages/profile/settings/notifications/telegram.tsx b/src/pages/profile/settings/notifications/telegram.tsx new file mode 100644 index 000000000..3a641aab3 --- /dev/null +++ b/src/pages/profile/settings/notifications/telegram.tsx @@ -0,0 +1,17 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsTelegram from '../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram'; + +const NotificationsPage: NextPage = () => { + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/pages/users/[userId]/settings/notifications.tsx b/src/pages/users/[userId]/settings/notifications.tsx deleted file mode 100644 index 08d9d62fb..000000000 --- a/src/pages/users/[userId]/settings/notifications.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { NextPage } from 'next'; -import React from 'react'; -import UserSettings from '../../../../components/UserProfile/UserSettings'; -import UserNotificationSettings from '../../../../components/UserProfile/UserSettings/UserNotificationSettings'; -import useRouteGuard from '../../../../hooks/useRouteGuard'; -import { Permission } from '../../../../hooks/useUser'; - -const UserSettingsMainPage: NextPage = () => { - useRouteGuard(Permission.MANAGE_USERS); - return ( - - - - ); -}; - -export default UserSettingsMainPage; diff --git a/src/pages/users/[userId]/settings/notifications/discord.tsx b/src/pages/users/[userId]/settings/notifications/discord.tsx new file mode 100644 index 000000000..f24b0810d --- /dev/null +++ b/src/pages/users/[userId]/settings/notifications/discord.tsx @@ -0,0 +1,20 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsDiscord from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsDiscord'; +import useRouteGuard from '../../../../../hooks/useRouteGuard'; +import { Permission } from '../../../../../hooks/useUser'; + +const NotificationsPage: NextPage = () => { + useRouteGuard(Permission.MANAGE_USERS); + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/pages/users/[userId]/settings/notifications/email.tsx b/src/pages/users/[userId]/settings/notifications/email.tsx new file mode 100644 index 000000000..7e62b1273 --- /dev/null +++ b/src/pages/users/[userId]/settings/notifications/email.tsx @@ -0,0 +1,20 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsEmail from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsEmail'; +import useRouteGuard from '../../../../../hooks/useRouteGuard'; +import { Permission } from '../../../../../hooks/useUser'; + +const NotificationsPage: NextPage = () => { + useRouteGuard(Permission.MANAGE_USERS); + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/pages/users/[userId]/settings/notifications/telegram.tsx b/src/pages/users/[userId]/settings/notifications/telegram.tsx new file mode 100644 index 000000000..d26ad8b4b --- /dev/null +++ b/src/pages/users/[userId]/settings/notifications/telegram.tsx @@ -0,0 +1,20 @@ +import { NextPage } from 'next'; +import React from 'react'; +import UserSettings from '../../../../../components/UserProfile/UserSettings'; +import UserNotificationSettings from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings'; +import UserNotificationsTelegram from '../../../../../components/UserProfile/UserSettings/UserNotificationSettings/UserNotificationsTelegram'; +import useRouteGuard from '../../../../../hooks/useRouteGuard'; +import { Permission } from '../../../../../hooks/useUser'; + +const NotificationsPage: NextPage = () => { + useRouteGuard(Permission.MANAGE_USERS); + return ( + + + + + + ); +}; + +export default NotificationsPage; diff --git a/src/styles/globals.css b/src/styles/globals.css index 848eae583..81e751348 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -217,14 +217,6 @@ img.avatar-sm { @apply flex max-w-lg rounded-md shadow-sm; } -.label-required { - @apply text-red-500; -} - -.label-tip { - @apply block text-gray-500; -} - .actions { @apply pt-5 mt-8 text-white border-t border-gray-700; } @@ -241,6 +233,18 @@ label.text-label { @apply sm:mt-2; } +label a { + @apply text-gray-100 transition duration-300 hover:text-white hover:underline; +} + +.label-required { + @apply ml-1 text-red-500; +} + +.label-tip { + @apply block text-gray-500; +} + button, input, select,