diff --git a/docs/using-seerr/notifications/webhook.md b/docs/using-seerr/notifications/webhook.md index 263058f9ae..8ed424c3bd 100644 --- a/docs/using-seerr/notifications/webhook.md +++ b/docs/using-seerr/notifications/webhook.md @@ -118,6 +118,7 @@ The following special variables are only included in request-related notificatio | `{{request_id}}` | The request ID | | `{{requestedBy_username}}` | The requesting user's username | | `{{requestedBy_email}}` | The requesting user's email address | +| `{{requestedBy_phoneNumber}}` | The requesting user's phone number | | `{{requestedBy_avatar}}` | The requesting user's avatar URL | | `{{requestedBy_jellyfinUserId}}` | The requesting user's Jellyfin User ID | | `{{requestedBy_settings_discordIds}}` | The requesting user's Discord ID(s) as a JSON array (if set) | @@ -134,6 +135,7 @@ The following special variables are only included in issue-related notifications | `{{issue_id}}` | The issue ID | | `{{reportedBy_username}}` | The requesting user's username | | `{{reportedBy_email}}` | The requesting user's email address | +| `{{reportedBy_phoneNumber}}` | The requesting user's phone number | | `{{reportedBy_avatar}}` | The requesting user's avatar URL | | `{{reportedBy_settings_discordIds}}` | The reporting user's Discord ID(s) as a JSON array (if set) | | `{{reportedBy_settings_telegramChatId}}` | The requesting user's Telegram Chat ID (if set) | @@ -149,6 +151,7 @@ The following special variables are only included in issue comment-related notif | `{{comment_message}}` | The comment message | | `{{commentedBy_username}}` | The commenting user's username | | `{{commentedBy_email}}` | The commenting user's email address | +| `{{commentedBy_phoneNumber}}` | The commenting user's phone number | | `{{commentedBy_avatar}}` | The commenting user's avatar URL | | `{{commentedBy_settings_discordIds}}` | The commenting user's Discord ID(s) as a JSON array (if set) | | `{{commentedBy_settings_telegramChatId}}` | The commenting user's Telegram Chat ID (if set) | diff --git a/seerr-api.yml b/seerr-api.yml index cd0012b679..86278c698c 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -4264,6 +4264,8 @@ paths: type: string permissions: type: number + phoneNumber: + type: string responses: '201': description: The created user diff --git a/server/constants/error.ts b/server/constants/error.ts index daa02f1a1c..1a985807f5 100644 --- a/server/constants/error.ts +++ b/server/constants/error.ts @@ -3,6 +3,7 @@ export enum ApiErrorCode { InvalidCredentials = 'INVALID_CREDENTIALS', InvalidAuthToken = 'INVALID_AUTH_TOKEN', InvalidEmail = 'INVALID_EMAIL', + InvalidPhoneNumber = 'INVALID_PHONE_NUMBER', NotAdmin = 'NOT_ADMIN', NoAdminUser = 'NO_ADMIN_USER', SyncErrorGroupedFolders = 'SYNC_ERROR_GROUPED_FOLDERS', diff --git a/server/entity/User.ts b/server/entity/User.ts index 644b38893f..197966f9b8 100644 --- a/server/entity/User.ts +++ b/server/entity/User.ts @@ -77,6 +77,9 @@ export class User { @Column({ nullable: true, select: false }) public password?: string; + @Column({ type: 'varchar', nullable: true, length: 20 }) + public phoneNumber?: string | null; + @Column({ nullable: true, select: false }) public resetPasswordGuid?: string; diff --git a/server/interfaces/api/userSettingsInterfaces.ts b/server/interfaces/api/userSettingsInterfaces.ts index 57e7b3f614..6f055c2330 100644 --- a/server/interfaces/api/userSettingsInterfaces.ts +++ b/server/interfaces/api/userSettingsInterfaces.ts @@ -3,6 +3,7 @@ import type { NotificationAgentKey } from '@server/lib/settings'; export interface UserSettingsGeneralResponse { username?: string; email?: string; + phoneNumber?: string; locale?: string; discoverRegion?: string; streamingRegion?: string; diff --git a/server/lib/notifications/agents/webhook.ts b/server/lib/notifications/agents/webhook.ts index 3fdde03ab8..9fe34297b3 100644 --- a/server/lib/notifications/agents/webhook.ts +++ b/server/lib/notifications/agents/webhook.ts @@ -41,6 +41,7 @@ const KeyMap: Record = { requestedBy_jellyfinUserId: 'request.requestedBy.jellyfinUserId', requestedBy_username: 'request.requestedBy.displayName', requestedBy_email: 'request.requestedBy.email', + requestedBy_phoneNumber: 'request.requestedBy.phoneNumber', requestedBy_avatar: 'request.requestedBy.avatar', requestedBy_settings_discordIds: 'request.requestedBy.settings.discordIds', requestedBy_settings_telegramChatId: @@ -52,12 +53,14 @@ const KeyMap: Record = { payload.issue ? IssueStatus[payload.issue.status] : '', reportedBy_username: 'issue.createdBy.displayName', reportedBy_email: 'issue.createdBy.email', + reportedBy_phoneNumber: 'issue.createdBy.phoneNumber', reportedBy_avatar: 'issue.createdBy.avatar', reportedBy_settings_discordIds: 'issue.createdBy.settings.discordIds', reportedBy_settings_telegramChatId: 'issue.createdBy.settings.telegramChatId', comment_message: 'comment.message', commentedBy_username: 'comment.user.displayName', commentedBy_email: 'comment.user.email', + commentedBy_phoneNumber: 'comment.user.phoneNumber', commentedBy_avatar: 'comment.user.avatar', commentedBy_settings_discordIds: 'comment.user.settings.discordIds', commentedBy_settings_telegramChatId: 'comment.user.settings.telegramChatId', diff --git a/server/migration/postgres/1783161650264-AddPhoneNumberToUser.ts b/server/migration/postgres/1783161650264-AddPhoneNumberToUser.ts new file mode 100644 index 0000000000..8210c7a1ee --- /dev/null +++ b/server/migration/postgres/1783161650264-AddPhoneNumberToUser.ts @@ -0,0 +1,15 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPhoneNumberToUser1783161650264 implements MigrationInterface { + name = 'AddPhoneNumberToUser1783161650264'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "user" ADD "phoneNumber" character varying(20)` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "phoneNumber"`); + } +} diff --git a/server/migration/sqlite/1783161620407-AddPhoneNumberToUser.ts b/server/migration/sqlite/1783161620407-AddPhoneNumberToUser.ts new file mode 100644 index 0000000000..9bcf3b4080 --- /dev/null +++ b/server/migration/sqlite/1783161620407-AddPhoneNumberToUser.ts @@ -0,0 +1,83 @@ +import type { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddPhoneNumberToUser1783161620407 implements MigrationInterface { + name = 'AddPhoneNumberToUser1783161620407'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + await queryRunner.query( + `CREATE TABLE "temporary_user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" datetime, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, "phoneNumber" varchar(20), CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "temporary_user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion" FROM "user"` + ); + await queryRunner.query(`DROP TABLE "user"`); + await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`); + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `CREATE TABLE "temporary_user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "temporary_user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "user_push_subscription"`); + await queryRunner.query( + `ALTER TABLE "temporary_user_push_subscription" RENAME TO "user_push_subscription"` + ); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + await queryRunner.query(`ALTER TABLE "user" RENAME TO "temporary_user"`); + await queryRunner.query( + `CREATE TABLE "user" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "email" varchar NOT NULL, "username" varchar, "plexId" integer, "plexToken" varchar, "permissions" integer NOT NULL DEFAULT (0), "avatar" varchar NOT NULL, "createdAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "updatedAt" datetime NOT NULL DEFAULT (CURRENT_TIMESTAMP), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" datetime, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, "jellyfinUsername" varchar, "jellyfinAuthToken" varchar, "jellyfinUserId" varchar, "jellyfinDeviceId" varchar, "avatarETag" varchar, "avatarVersion" varchar, CONSTRAINT "UQ_e12875dfb3b1d92d7d7c5377e22" UNIQUE ("email"))` + ); + await queryRunner.query( + `INSERT INTO "user"("id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate", "movieQuotaLimit", "movieQuotaDays", "tvQuotaLimit", "tvQuotaDays", "jellyfinUsername", "jellyfinAuthToken", "jellyfinUserId", "jellyfinDeviceId", "avatarETag", "avatarVersion" FROM "temporary_user"` + ); + await queryRunner.query(`DROP TABLE "temporary_user"`); + await queryRunner.query(`DROP INDEX "IDX_03f7958328e311761b0de675fb"`); + await queryRunner.query( + `ALTER TABLE "user_push_subscription" RENAME TO "temporary_user_push_subscription"` + ); + await queryRunner.query( + `CREATE TABLE "user_push_subscription" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "endpoint" varchar NOT NULL, "p256dh" varchar NOT NULL, "auth" varchar NOT NULL, "userId" integer, "userAgent" varchar, "createdAt" datetime DEFAULT (CURRENT_TIMESTAMP), CONSTRAINT "UQ_6427d07d9a171a3a1ab87480005" UNIQUE ("endpoint", "userId"), CONSTRAINT "UQ_f90ab5a4ed54905a4bb51a7148b" UNIQUE ("auth"), CONSTRAINT "FK_03f7958328e311761b0de675fbe" FOREIGN KEY ("userId") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)` + ); + await queryRunner.query( + `INSERT INTO "user_push_subscription"("id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt") SELECT "id", "endpoint", "p256dh", "auth", "userId", "userAgent", "createdAt" FROM "temporary_user_push_subscription"` + ); + await queryRunner.query(`DROP TABLE "temporary_user_push_subscription"`); + await queryRunner.query( + `CREATE INDEX "IDX_03f7958328e311761b0de675fb" ON "user_push_subscription" ("userId") ` + ); + } +} diff --git a/server/routes/user/index.ts b/server/routes/user/index.ts index 0d5843f85a..8cc646aa23 100644 --- a/server/routes/user/index.ts +++ b/server/routes/user/index.ts @@ -211,6 +211,7 @@ router.post( permissions: settings.main.defaultPermissions, plexToken: '', userType: UserType.LOCAL, + phoneNumber: body.phoneNumber, }); if (passedExplicitPassword) { diff --git a/server/routes/user/usersettings.ts b/server/routes/user/usersettings.ts index bff500d58a..0a6e35e96f 100644 --- a/server/routes/user/usersettings.ts +++ b/server/routes/user/usersettings.ts @@ -24,8 +24,11 @@ import { import { Router } from 'express'; import net from 'net'; import { Not } from 'typeorm'; +import { z } from 'zod'; import { canMakePermissionsChange } from '.'; +const phoneNumberSchema = z.string().regex(/^\+?[0-9]{1,15}$/); + const userSettingsRoutes = Router({ mergeParams: true }); userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( @@ -49,6 +52,7 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>( return res.status(200).json({ username: user.username, email: user.email, + phoneNumber: user.phoneNumber ?? '', locale: user.settings?.locale, discoverRegion: user.settings?.discoverRegion, streamingRegion: user.settings?.streamingRegion, @@ -108,6 +112,16 @@ userSettingsRoutes.post< throw new ApiError(400, ApiErrorCode.InvalidEmail); } + if (req.body.phoneNumber) { + const result = phoneNumberSchema.safeParse(req.body.phoneNumber); + if (!result.success) { + throw new ApiError(400, ApiErrorCode.InvalidPhoneNumber); + } + user.phoneNumber = result.data; + } else { + user.phoneNumber = ''; + } + // Update quota values only if the user has the correct permissions if ( !user.hasPermission(Permission.MANAGE_USERS) && @@ -149,6 +163,7 @@ userSettingsRoutes.post< watchlistSyncMovies: savedUser.settings?.watchlistSyncMovies, watchlistSyncTv: savedUser.settings?.watchlistSyncTv, email: savedUser.email, + phoneNumber: savedUser.phoneNumber ?? '', }); } catch (e) { if (e.errorCode) { diff --git a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx index d69a6c9c4f..d3817489cb 100644 --- a/src/components/Settings/Notifications/NotificationsWebhook/index.tsx +++ b/src/components/Settings/Notifications/NotificationsWebhook/index.tsx @@ -48,6 +48,7 @@ const defaultPayload = { request_id: '{{request_id}}', requestedBy_email: '{{requestedBy_email}}', requestedBy_username: '{{requestedBy_username}}', + requestedBy_phoneNumber: '{{requestedBy_phoneNumber}}', requestedBy_avatar: '{{requestedBy_avatar}}', requestedBy_jellyfinUserId: '{{requestedBy_jellyfinUserId}}', requestedBy_settings_discordIds: '{{requestedBy_settings_discordIds}}', @@ -60,6 +61,7 @@ const defaultPayload = { issue_status: '{{issue_status}}', reportedBy_email: '{{reportedBy_email}}', reportedBy_username: '{{reportedBy_username}}', + reportedBy_phoneNumber: '{{reportedBy_phoneNumber}}', reportedBy_avatar: '{{reportedBy_avatar}}', reportedBy_settings_discordIds: '{{reportedBy_settings_discordIds}}', reportedBy_settings_telegramChatId: @@ -69,6 +71,7 @@ const defaultPayload = { comment_message: '{{comment_message}}', commentedBy_email: '{{commentedBy_email}}', commentedBy_username: '{{commentedBy_username}}', + commentedBy_phoneNumber: '{{commentedBy_phoneNumber}}', commentedBy_avatar: '{{commentedBy_avatar}}', commentedBy_settings_discordIds: '{{commentedBy_settings_discordIds}}', commentedBy_settings_telegramChatId: diff --git a/src/components/UserList/index.tsx b/src/components/UserList/index.tsx index c36d7d692a..70f7af1a37 100644 --- a/src/components/UserList/index.tsx +++ b/src/components/UserList/index.tsx @@ -17,6 +17,7 @@ import type { User } from '@app/hooks/useUser'; import { Permission, UserType, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import defineMessages from '@app/utils/defineMessages'; +import { PHONE_REGEX } from '@app/utils/numberHelpers'; import { Transition } from '@headlessui/react'; import { BarsArrowDownIcon, @@ -75,6 +76,8 @@ const messages = defineMessages('components.UserList', { username: 'Username', email: 'Email Address', password: 'Password', + phoneNumber: 'Phone number', + validationPhoneNumber: 'Invalid phone number', passwordinfodescription: 'Configure an application URL and enable email notifications to allow automatic password generation.', autogeneratepassword: 'Automatically Generate Password', @@ -326,6 +329,10 @@ const UserList = () => { intl.formatMessage(messages.validationpasswordminchars) ) ), + phoneNumber: Yup.string().matches(PHONE_REGEX, { + message: intl.formatMessage(messages.validationPhoneNumber), + excludeEmptyString: true, + }), }); if (!data) { @@ -386,6 +393,7 @@ const UserList = () => { username: '', email: '', password: '', + phoneNumber: '', genpassword: false, }} validationSchema={CreateUserSchema} @@ -395,6 +403,7 @@ const UserList = () => { username: values.username, email: values.email, password: values.genpassword ? null : values.password, + phoneNumber: values.phoneNumber, }); addToast(intl.formatMessage(messages.usercreatedsuccess), { appearance: 'success', @@ -554,6 +563,21 @@ const UserList = () => { )} +
+ +
+
+ +
+ {errors.phoneNumber && + touched.phoneNumber && + typeof errors.phoneNumber === 'string' && ( +
{errors.phoneNumber}
+ )} +
+
); diff --git a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx index c0e9100869..0784b4cb23 100644 --- a/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx +++ b/src/components/UserProfile/UserSettings/UserGeneralSettings/index.tsx @@ -13,6 +13,7 @@ import { Permission, UserType, useUser } from '@app/hooks/useUser'; import globalMessages from '@app/i18n/globalMessages'; import ErrorPage from '@app/pages/_error'; import defineMessages from '@app/utils/defineMessages'; +import { PHONE_REGEX } from '@app/utils/numberHelpers'; import { ArrowDownOnSquareIcon } from '@heroicons/react/24/outline'; import { ApiErrorCode } from '@server/constants/error'; import type { UserSettingsGeneralResponse } from '@server/interfaces/api/userSettingsInterfaces'; @@ -33,6 +34,7 @@ const messages = defineMessages( generalsettings: 'General Settings', displayName: 'Display Name', email: 'Email', + phoneNumber: 'Phone number', save: 'Save Changes', saving: 'Saving…', mediaServerUser: '{mediaServerName} User', @@ -63,6 +65,7 @@ const messages = defineMessages( languageDefault: 'Default ({language})', validationemailrequired: 'Email required', validationemailformat: 'Valid email required', + validationPhoneNumber: 'Invalid phone number', plexwatchlistsyncmovies: 'Auto-Request Movies', plexwatchlistsyncmoviestip: 'Automatically request movies on your Plex Watchlist', @@ -115,6 +118,10 @@ const UserGeneralSettings = () => { (value) => !value || validator.isEmail(value, { require_tld: false }) ), + phoneNumber: Yup.string().matches(PHONE_REGEX, { + message: intl.formatMessage(messages.validationPhoneNumber), + excludeEmptyString: true, + }), }); useEffect(() => { @@ -151,6 +158,7 @@ const UserGeneralSettings = () => { initialValues={{ displayName: data?.username !== user?.email ? data?.username : '', email: data?.email?.includes('@') ? data.email : '', + phoneNumber: data?.phoneNumber ?? '', locale: data?.locale, discoverRegion: data?.discoverRegion, streamingRegion: data?.streamingRegion, @@ -170,6 +178,7 @@ const UserGeneralSettings = () => { username: values.displayName, email: values.email || user?.jellyfinUsername || user?.plexUsername, + phoneNumber: values.phoneNumber, locale: values.locale, discoverRegion: values.discoverRegion, streamingRegion: values.streamingRegion, @@ -332,6 +341,19 @@ const UserGeneralSettings = () => { )} +
+ +
+
+ +
+ {errors.phoneNumber && touched.phoneNumber && ( +
{errors.phoneNumber}
+ )} +
+