Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/using-seerr/notifications/webhook.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand All @@ -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) |
Expand All @@ -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) |
2 changes: 2 additions & 0 deletions seerr-api.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4264,6 +4264,8 @@ paths:
type: string
permissions:
type: number
phoneNumber:
type: string
Comment thread
coderabbitai[bot] marked this conversation as resolved.
responses:
'201':
description: The created user
Expand Down
1 change: 1 addition & 0 deletions server/constants/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
3 changes: 3 additions & 0 deletions server/entity/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
@Column({ nullable: true, select: false })
public resetPasswordGuid?: string;

Expand Down
1 change: 1 addition & 0 deletions server/interfaces/api/userSettingsInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions server/lib/notifications/agents/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
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:
Expand All @@ -52,12 +53,14 @@ const KeyMap: Record<string, string | KeyMapFunction> = {
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',
Expand Down
15 changes: 15 additions & 0 deletions server/migration/postgres/1783161650264-AddPhoneNumberToUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddPhoneNumberToUser1783161650264 implements MigrationInterface {
name = 'AddPhoneNumberToUser1783161650264';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user" ADD "phoneNumber" character varying(20)`
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "phoneNumber"`);
}
}
83 changes: 83 additions & 0 deletions server/migration/sqlite/1783161620407-AddPhoneNumberToUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { MigrationInterface, QueryRunner } from 'typeorm';

export class AddPhoneNumberToUser1783161620407 implements MigrationInterface {
name = 'AddPhoneNumberToUser1783161620407';

public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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") `
);
}
}
1 change: 1 addition & 0 deletions server/routes/user/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ router.post(
permissions: settings.main.defaultPermissions,
plexToken: '',
userType: UserType.LOCAL,
phoneNumber: body.phoneNumber,
});

if (passedExplicitPassword) {
Expand Down
15 changes: 15 additions & 0 deletions server/routes/user/usersettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand All @@ -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,
Expand Down Expand Up @@ -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) &&
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}}',
Expand All @@ -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:
Expand All @@ -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:
Expand Down
Loading