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 | 26 +- .../interfaces/api/userSettingsInterfaces.ts | 9 +- server/lib/email/index.ts | 9 +- server/lib/notifications/agents/discord.ts | 58 +- server/lib/notifications/agents/email.ts | 529 ++++++------------ 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 | 123 ++-- 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 | 107 ++-- .../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, 1725 insertions(+), 1499 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 965a903c..08bf1b5c 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 ebc9b9fd..167d1db0 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 5c392706..25b57f71 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 8e60865a..023a1bde 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,24 +24,28 @@ export class UserSettings { @JoinColumn() public user: User; - @Column({ default: true }) - public enableNotifications: boolean; - @Column({ nullable: true }) - public discordId?: string; + public region?: string; @Column({ nullable: true }) - public telegramChatId?: string; + public originalLanguage?: string; + + @Column({ type: 'integer', default: NotificationAgentType.EMAIL }) + public notificationAgents = NotificationAgentType.EMAIL; @Column({ nullable: true }) - public telegramSendSilently?: boolean; + public pgpKey?: string; @Column({ nullable: true }) - public region?: string; + public discordId?: string; @Column({ nullable: true }) - public originalLanguage?: string; + public telegramChatId?: string; @Column({ nullable: true }) - public pgpKey?: string; + public telegramSendSilently?: boolean; + + 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 e6d0302f..006facf0 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 abbc1632..f9c0c747 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 cefde186..c04b4948 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 ea6b02ef..4d00eb6f 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 - 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; - } - } - - private async sendMediaFailedEmail(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 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; + 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 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`, - }, - }); + if (payload.media) { + let requestType = ''; + let body = ''; + + 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; - } + return undefined; } - 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 { + public async send( + type: Notification, + payload: NotificationPayload + ): Promise { + if (payload.notifyUser) { + // Send notification to the user who submitted the request if ( - payload.notifyUser && - (payload.notifyUser.settings?.enableNotifications ?? true) + !payload.notifyUser.settings || + payload.notifyUser.settings.hasNotificationAgentEnabled( + NotificationAgentType.EMAIL + ) ) { - 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`, - }, + logger.debug('Sending email notification', { + label: 'Notifications', + recipient: payload.notifyUser.displayName, + type: Notification[type], + subject: payload.subject, }); - } - 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); + 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, + }); - await email.send({ - template: path.join(__dirname, '../../../templates/email/test-email'), - message: { - to: payload.notifyUser.email, - }, - locals: { - body: payload.message, - applicationUrl, - applicationTitle, - }, - }); + return false; + } } + } else { + // Send notifications to all users with the Manage Requests permission + const userRepository = getRepository(User); + const users = await userRepository.find(); - return true; - } catch (e) { - logger.error('Email notification failed to send', { - label: 'Notifications', - message: e.message, - }); - return false; - } - } - - public async send( - type: Notification, - payload: NotificationPayload - ): Promise { - logger.debug('Sending email notification', { label: 'Notifications' }); - - 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; + 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 f0c0f757..c43e9971 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 3b5d3f87..f9bff21c 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 b5234785..f9fe46c9 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 5fa4c518..894a7726 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'}`; - - // 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, { + logger.debug('Sending Telegram notification', { + label: 'Notifications', + type: Notification[type], + subject: payload.subject, + }); + + 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 b5c98923..7630cf44 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 00000000..9e0d79aa --- /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 7d5b6800..f1f237f5 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 00000000..86a52c08 --- /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 fbf1ce1e..739b3981 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 693c228e..f85ef179 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) }, - }); + try { + const user = await userRepository.findOne({ + where: { id: Number(req.params.id) }, + }); - if (!user) { - return next({ status: 404, message: 'User not found.' }); - } + 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.", - }); - } + // "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, - enableNotifications: req.body.enableNotifications, - discordId: req.body.discordId, - telegramChatId: req.body.telegramChatId, - telegramSendSilently: req.body.telegramSendSilently, - pgpKey: req.body.pgpKey, - }); - } 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; - } + 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); + 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 }); + return res.status(200).json({ + notificationAgents: user.settings?.notificationAgents, + pgpKey: user.settings?.pgpKey, + discordId: user.settings?.discordId, + telegramChatId: user.settings?.telegramChatId, + telegramSendSilently: user?.settings?.telegramSendSilently, + }); + } catch (e) { + next({ status: 500, message: e.message }); + } } -}); +); userSettingsRoutes.get<{ id: string }, { permissions?: number }>( '/permissions', diff --git a/server/templates/email/generatedpassword/html.pug b/server/templates/email/generatedpassword/html.pug index 1fa4713f..b9bc2a2e 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 f7c8bb08..718a0495 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 b4abfebb..f1b21b36 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 00000000..2e47b418 --- /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 85224717..4085b2a6 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 04e74323..625062bf 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 f5e940fb..dbb95afe 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 8b07b900..f7fb621e 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 158059ce..ced53a40 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 00e8e443..b8867513 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 a2da3cbf..4f339fff 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 dd13cfe4..65f4d548 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 a6893a38..761c7327 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 00000000..244e1d0d --- /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 00000000..b949fb95 --- /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 00000000..6193e127 --- /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 df828e5b..b52db481 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 d6babb63..8863495f 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 8e83a4c9..8c9033f0 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 2e737d55..867303f1 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 f0d67ffb..dfa06262 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 dcb27361..00000000 --- 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 00000000..06e580ff --- /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 00000000..370258ca --- /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 00000000..3a641aab --- /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 08d9d62f..00000000 --- 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 00000000..f24b0810 --- /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 00000000..7e62b127 --- /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 00000000..d26ad8b4 --- /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 848eae58..81e75134 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,