mirror of
https://github.com/sct/overseerr.git
synced 2025-10-03 00:48:07 +02:00
feat(requests): add request quotas (#1277)
* feat(quotas): rebased * feat: add getQuota() method to User entity * feat(ui): add default quota setting options * feat: user quota settings * feat: quota display in request modals * fix: only show user quotas on own profile or with manage users permission * feat: add request progress circles to profile page * feat: add migration * fix: add missing restricted field to api schema * fix: dont show auto approve message for movie request when restricted * fix(lang): change enable checkbox langauge to "enable override" Co-authored-by: Jakob Ankarhem <jakob.ankarhem@outlook.com> Co-authored-by: TheCatLady <52870424+TheCatLady@users.noreply.github.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
getRepository,
|
||||
OneToMany,
|
||||
AfterRemove,
|
||||
RelationCount,
|
||||
} from 'typeorm';
|
||||
import { User } from './User';
|
||||
import Media from './Media';
|
||||
@@ -60,6 +61,9 @@ export class MediaRequest {
|
||||
@Column({ type: 'varchar' })
|
||||
public type: MediaType;
|
||||
|
||||
@RelationCount((request: MediaRequest) => request.seasons)
|
||||
public seasonCount: number;
|
||||
|
||||
@OneToMany(() => SeasonRequest, (season) => season.request, {
|
||||
eager: true,
|
||||
cascade: true,
|
||||
|
@@ -1,28 +1,34 @@
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
OneToMany,
|
||||
RelationCount,
|
||||
AfterLoad,
|
||||
OneToOne,
|
||||
} from 'typeorm';
|
||||
import {
|
||||
Permission,
|
||||
hasPermission,
|
||||
PermissionCheckOptions,
|
||||
} from '../lib/permissions';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import bcrypt from 'bcrypt';
|
||||
import path from 'path';
|
||||
import PreparedEmail from '../lib/email';
|
||||
import logger from '../logger';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import { default as generatePassword } from 'secure-random-password';
|
||||
import { UserType } from '../constants/user';
|
||||
import {
|
||||
AfterLoad,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
getRepository,
|
||||
MoreThan,
|
||||
Not,
|
||||
OneToMany,
|
||||
OneToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
RelationCount,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { MediaRequestStatus, MediaType } from '../constants/media';
|
||||
import { UserType } from '../constants/user';
|
||||
import { QuotaResponse } from '../interfaces/api/userInterfaces';
|
||||
import PreparedEmail from '../lib/email';
|
||||
import {
|
||||
hasPermission,
|
||||
Permission,
|
||||
PermissionCheckOptions,
|
||||
} from '../lib/permissions';
|
||||
import { getSettings } from '../lib/settings';
|
||||
import logger from '../logger';
|
||||
import { MediaRequest } from './MediaRequest';
|
||||
import SeasonRequest from './SeasonRequest';
|
||||
import { UserSettings } from './UserSettings';
|
||||
|
||||
@Entity()
|
||||
@@ -80,6 +86,18 @@ export class User {
|
||||
@OneToMany(() => MediaRequest, (request) => request.requestedBy)
|
||||
public requests: MediaRequest[];
|
||||
|
||||
@Column({ nullable: true })
|
||||
public movieQuotaLimit?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public movieQuotaDays?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public tvQuotaLimit?: number;
|
||||
|
||||
@Column({ nullable: true })
|
||||
public tvQuotaDays?: number;
|
||||
|
||||
@OneToOne(() => UserSettings, (settings) => settings.user, {
|
||||
cascade: true,
|
||||
eager: true,
|
||||
@@ -199,4 +217,105 @@ export class User {
|
||||
public setDisplayName(): void {
|
||||
this.displayName = this.username || this.plexUsername;
|
||||
}
|
||||
|
||||
public async getQuota(): Promise<QuotaResponse> {
|
||||
const {
|
||||
main: { defaultQuotas },
|
||||
} = getSettings();
|
||||
const requestRepository = getRepository(MediaRequest);
|
||||
const canBypass = this.hasPermission([Permission.MANAGE_USERS], {
|
||||
type: 'or',
|
||||
});
|
||||
|
||||
const movieQuotaLimit = !canBypass
|
||||
? this.movieQuotaLimit ?? defaultQuotas.movie.quotaLimit
|
||||
: 0;
|
||||
const movieQuotaDays = this.movieQuotaDays ?? defaultQuotas.movie.quotaDays;
|
||||
|
||||
// Count movie requests made during quota period
|
||||
const movieDate = new Date();
|
||||
if (movieQuotaDays) {
|
||||
movieDate.setDate(movieDate.getDate() - movieQuotaDays);
|
||||
} else {
|
||||
movieDate.setDate(0);
|
||||
}
|
||||
// YYYY-MM-DD format
|
||||
const movieQuotaStartDate = movieDate.toJSON().split('T')[0];
|
||||
const movieQuotaUsed = movieQuotaLimit
|
||||
? await requestRepository.count({
|
||||
where: {
|
||||
requestedBy: this,
|
||||
createdAt: MoreThan(movieQuotaStartDate),
|
||||
type: MediaType.MOVIE,
|
||||
status: Not(MediaRequestStatus.DECLINED),
|
||||
},
|
||||
})
|
||||
: 0;
|
||||
|
||||
const tvQuotaLimit = !canBypass
|
||||
? this.tvQuotaLimit ?? defaultQuotas.tv.quotaLimit
|
||||
: 0;
|
||||
const tvQuotaDays = this.tvQuotaDays ?? defaultQuotas.tv.quotaDays;
|
||||
|
||||
// Count tv season requests made during quota period
|
||||
const tvDate = new Date();
|
||||
if (tvQuotaDays) {
|
||||
tvDate.setDate(tvDate.getDate() - tvQuotaDays);
|
||||
} else {
|
||||
tvDate.setDate(0);
|
||||
}
|
||||
// YYYY-MM-DD format
|
||||
const tvQuotaStartDate = tvDate.toJSON().split('T')[0];
|
||||
const tvQuotaUsed = tvQuotaLimit
|
||||
? (
|
||||
await requestRepository
|
||||
.createQueryBuilder('request')
|
||||
.leftJoin('request.seasons', 'seasons')
|
||||
.leftJoin('request.requestedBy', 'requestedBy')
|
||||
.where('request.type = :requestType', {
|
||||
requestType: MediaType.TV,
|
||||
})
|
||||
.andWhere('requestedBy.id = :userId', {
|
||||
userId: this.id,
|
||||
})
|
||||
.andWhere('request.createdAt > :date', {
|
||||
date: tvQuotaStartDate,
|
||||
})
|
||||
.andWhere('request.status != :declinedStatus', {
|
||||
declinedStatus: MediaRequestStatus.DECLINED,
|
||||
})
|
||||
.addSelect((subQuery) => {
|
||||
return subQuery
|
||||
.select('COUNT(season.id)', 'seasonCount')
|
||||
.from(SeasonRequest, 'season')
|
||||
.leftJoin('season.request', 'parentRequest')
|
||||
.where('parentRequest.id = request.id');
|
||||
}, 'seasonCount')
|
||||
.getMany()
|
||||
).reduce((sum: number, req: MediaRequest) => sum + req.seasonCount, 0)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
movie: {
|
||||
days: movieQuotaDays,
|
||||
limit: movieQuotaLimit,
|
||||
used: movieQuotaUsed,
|
||||
remaining: movieQuotaLimit
|
||||
? movieQuotaLimit - movieQuotaUsed
|
||||
: undefined,
|
||||
restricted:
|
||||
movieQuotaLimit && movieQuotaLimit - movieQuotaUsed <= 0
|
||||
? true
|
||||
: false,
|
||||
},
|
||||
tv: {
|
||||
days: tvQuotaDays,
|
||||
limit: tvQuotaLimit,
|
||||
used: tvQuotaUsed,
|
||||
remaining: tvQuotaLimit ? tvQuotaLimit - tvQuotaUsed : undefined,
|
||||
restricted:
|
||||
tvQuotaLimit && tvQuotaLimit - tvQuotaUsed <= 0 ? true : false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { User } from '../../entity/User';
|
||||
import { MediaRequest } from '../../entity/MediaRequest';
|
||||
import type { User } from '../../entity/User';
|
||||
import { PaginatedResponse } from './common';
|
||||
|
||||
export interface UserResultsResponse extends PaginatedResponse {
|
||||
@@ -9,3 +9,16 @@ export interface UserResultsResponse extends PaginatedResponse {
|
||||
export interface UserRequestsResponse extends PaginatedResponse {
|
||||
results: MediaRequest[];
|
||||
}
|
||||
|
||||
export interface QuotaStatus {
|
||||
days?: number;
|
||||
limit?: number;
|
||||
used: number;
|
||||
remaining?: number;
|
||||
restricted: boolean;
|
||||
}
|
||||
|
||||
export interface QuotaResponse {
|
||||
movie: QuotaStatus;
|
||||
tv: QuotaStatus;
|
||||
}
|
||||
|
@@ -2,6 +2,14 @@ export interface UserSettingsGeneralResponse {
|
||||
username?: string;
|
||||
region?: string;
|
||||
originalLanguage?: string;
|
||||
movieQuotaLimit?: number;
|
||||
movieQuotaDays?: number;
|
||||
tvQuotaLimit?: number;
|
||||
tvQuotaDays?: number;
|
||||
globalMovieQuotaDays?: number;
|
||||
globalMovieQuotaLimit?: number;
|
||||
globalTvQuotaLimit?: number;
|
||||
globalTvQuotaDays?: number;
|
||||
}
|
||||
|
||||
export interface UserSettingsNotificationsResponse {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { merge } from 'lodash';
|
||||
import path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { Permission } from './permissions';
|
||||
|
||||
@@ -61,6 +61,11 @@ export interface SonarrSettings extends DVRSettings {
|
||||
enableSeasonFolders: boolean;
|
||||
}
|
||||
|
||||
interface Quota {
|
||||
quotaLimit?: number;
|
||||
quotaDays?: number;
|
||||
}
|
||||
|
||||
export interface MainSettings {
|
||||
apiKey: string;
|
||||
applicationTitle: string;
|
||||
@@ -68,6 +73,10 @@ export interface MainSettings {
|
||||
csrfProtection: boolean;
|
||||
cacheImages: boolean;
|
||||
defaultPermissions: number;
|
||||
defaultQuotas: {
|
||||
movie: Quota;
|
||||
tv: Quota;
|
||||
};
|
||||
hideAvailable: boolean;
|
||||
localLogin: boolean;
|
||||
region: string;
|
||||
@@ -199,6 +208,10 @@ class Settings {
|
||||
csrfProtection: false,
|
||||
cacheImages: false,
|
||||
defaultPermissions: Permission.REQUEST,
|
||||
defaultQuotas: {
|
||||
movie: {},
|
||||
tv: {},
|
||||
},
|
||||
hideAvailable: false,
|
||||
localLogin: true,
|
||||
region: '',
|
||||
|
27
server/migration/1616576677254-AddUserQuotaFields.ts
Normal file
27
server/migration/1616576677254-AddUserQuotaFields.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddUserQuotaFields1616576677254 implements MigrationInterface {
|
||||
name = 'AddUserQuotaFields1616576677254';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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 (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, "movieQuotaLimit" integer, "movieQuotaDays" integer, "tvQuotaLimit" integer, "tvQuotaDays" integer, 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") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "user"`);
|
||||
await queryRunner.query(`ALTER TABLE "temporary_user" RENAME TO "user"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
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 (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "password" varchar, "userType" integer NOT NULL DEFAULT (1), "plexUsername" varchar, "resetPasswordGuid" varchar, "recoveryLinkExpirationDate" date, 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") SELECT "id", "email", "username", "plexId", "plexToken", "permissions", "avatar", "createdAt", "updatedAt", "password", "userType", "plexUsername", "resetPasswordGuid", "recoveryLinkExpirationDate" FROM "temporary_user"`
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "temporary_user"`);
|
||||
}
|
||||
}
|
@@ -1,15 +1,15 @@
|
||||
import { Router } from 'express';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import TheMovieDb from '../api/themoviedb';
|
||||
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
|
||||
import Media from '../entity/Media';
|
||||
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
|
||||
import { MediaRequest } from '../entity/MediaRequest';
|
||||
import SeasonRequest from '../entity/SeasonRequest';
|
||||
import logger from '../logger';
|
||||
import { RequestResultsResponse } from '../interfaces/api/requestInterfaces';
|
||||
import { User } from '../entity/User';
|
||||
import { RequestResultsResponse } from '../interfaces/api/requestInterfaces';
|
||||
import { Permission } from '../lib/permissions';
|
||||
import logger from '../logger';
|
||||
import { isAuthenticated } from '../middleware/auth';
|
||||
|
||||
const requestRoutes = Router();
|
||||
|
||||
@@ -154,8 +154,29 @@ requestRoutes.post(
|
||||
});
|
||||
}
|
||||
|
||||
if (!requestUser) {
|
||||
return next({
|
||||
status: 500,
|
||||
message: 'User missing from request context.',
|
||||
});
|
||||
}
|
||||
|
||||
const quotas = await requestUser.getQuota();
|
||||
|
||||
if (req.body.mediaType === MediaType.MOVIE && quotas.movie.restricted) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Movie Quota Exceeded',
|
||||
});
|
||||
} else if (req.body.mediaType === MediaType.TV && quotas.tv.restricted) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'Series Quota Exceeded',
|
||||
});
|
||||
}
|
||||
|
||||
const tmdbMedia =
|
||||
req.body.mediaType === 'movie'
|
||||
req.body.mediaType === MediaType.MOVIE
|
||||
? await tmdb.getMovie({ movieId: req.body.mediaId })
|
||||
: await tmdb.getTvShow({ tvId: req.body.mediaId });
|
||||
|
||||
@@ -182,7 +203,7 @@ requestRoutes.post(
|
||||
}
|
||||
}
|
||||
|
||||
if (req.body.mediaType === 'movie') {
|
||||
if (req.body.mediaType === MediaType.MOVIE) {
|
||||
const existing = await requestRepository.findOne({
|
||||
where: {
|
||||
media: {
|
||||
@@ -247,7 +268,7 @@ requestRoutes.post(
|
||||
|
||||
await requestRepository.save(request);
|
||||
return res.status(201).json(request);
|
||||
} else if (req.body.mediaType === 'tv') {
|
||||
} else if (req.body.mediaType === MediaType.TV) {
|
||||
const requestedSeasons = req.body.seasons as number[];
|
||||
let existingSeasons: number[] = [];
|
||||
|
||||
@@ -458,14 +479,14 @@ requestRoutes.put<{ requestId: string }>(
|
||||
});
|
||||
}
|
||||
|
||||
if (req.body.mediaType === 'movie') {
|
||||
if (req.body.mediaType === MediaType.MOVIE) {
|
||||
request.serverId = req.body.serverId;
|
||||
request.profileId = req.body.profileId;
|
||||
request.rootFolder = req.body.rootFolder;
|
||||
request.requestedBy = requestUser as User;
|
||||
|
||||
requestRepository.save(request);
|
||||
} else if (req.body.mediaType === 'tv') {
|
||||
} else if (req.body.mediaType === MediaType.TV) {
|
||||
const mediaRepository = getRepository(Media);
|
||||
request.serverId = req.body.serverId;
|
||||
request.profileId = req.body.profileId;
|
||||
|
@@ -1,16 +1,19 @@
|
||||
import { Router } from 'express';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { getRepository, Not } from 'typeorm';
|
||||
import PlexTvAPI from '../../api/plextv';
|
||||
import { UserType } from '../../constants/user';
|
||||
import { MediaRequest } from '../../entity/MediaRequest';
|
||||
import { User } from '../../entity/User';
|
||||
import {
|
||||
QuotaResponse,
|
||||
UserRequestsResponse,
|
||||
UserResultsResponse,
|
||||
} from '../../interfaces/api/userInterfaces';
|
||||
import { hasPermission, Permission } from '../../lib/permissions';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import gravatarUrl from 'gravatar-url';
|
||||
import { UserType } from '../../constants/user';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
import { UserResultsResponse } from '../../interfaces/api/userInterfaces';
|
||||
import { UserRequestsResponse } from '../../interfaces/api/userInterfaces';
|
||||
import userSettingsRoutes from './usersettings';
|
||||
|
||||
const router = Router();
|
||||
@@ -380,4 +383,36 @@ router.post(
|
||||
}
|
||||
);
|
||||
|
||||
router.get<{ id: string }, QuotaResponse>(
|
||||
'/:id/quota',
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
if (
|
||||
Number(req.params.id) !== req.user?.id &&
|
||||
!req.user?.hasPermission(
|
||||
[Permission.MANAGE_USERS, Permission.MANAGE_REQUESTS],
|
||||
{ type: 'and' }
|
||||
)
|
||||
) {
|
||||
return next({
|
||||
status: 403,
|
||||
message: 'You do not have permission to access this endpoint.',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await userRepository.findOneOrFail({
|
||||
where: { id: Number(req.params.id) },
|
||||
});
|
||||
|
||||
const quotas = await user.getQuota();
|
||||
|
||||
return res.status(200).json(quotas);
|
||||
} catch (e) {
|
||||
next({ status: 404, message: e.message });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
@@ -2,13 +2,13 @@ import { Router } from 'express';
|
||||
import { getRepository } from 'typeorm';
|
||||
import { canMakePermissionsChange } from '.';
|
||||
import { User } from '../../entity/User';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
import { UserSettings } from '../../entity/UserSettings';
|
||||
import {
|
||||
UserSettingsGeneralResponse,
|
||||
UserSettingsNotificationsResponse,
|
||||
} from '../../interfaces/api/userSettingsInterfaces';
|
||||
import { Permission } from '../../lib/permissions';
|
||||
import { getSettings } from '../../lib/settings';
|
||||
import logger from '../../logger';
|
||||
import { isAuthenticated } from '../../middleware/auth';
|
||||
|
||||
@@ -35,6 +35,9 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
|
||||
'/main',
|
||||
isOwnProfileOrAdmin(),
|
||||
async (req, res, next) => {
|
||||
const {
|
||||
main: { defaultQuotas },
|
||||
} = getSettings();
|
||||
const userRepository = getRepository(User);
|
||||
|
||||
try {
|
||||
@@ -50,6 +53,14 @@ userSettingsRoutes.get<{ id: string }, UserSettingsGeneralResponse>(
|
||||
username: user.username,
|
||||
region: user.settings?.region,
|
||||
originalLanguage: user.settings?.originalLanguage,
|
||||
movieQuotaLimit: user.movieQuotaLimit,
|
||||
movieQuotaDays: user.movieQuotaDays,
|
||||
tvQuotaLimit: user.tvQuotaLimit,
|
||||
tvQuotaDays: user.tvQuotaDays,
|
||||
globalMovieQuotaDays: defaultQuotas.movie.quotaDays,
|
||||
globalMovieQuotaLimit: defaultQuotas.movie.quotaLimit,
|
||||
globalTvQuotaDays: defaultQuotas.tv.quotaDays,
|
||||
globalTvQuotaLimit: defaultQuotas.tv.quotaLimit,
|
||||
});
|
||||
} catch (e) {
|
||||
next({ status: 500, message: e.message });
|
||||
@@ -82,6 +93,18 @@ userSettingsRoutes.post<
|
||||
}
|
||||
|
||||
user.username = req.body.username;
|
||||
|
||||
// Update quota values only if the user has the correct permissions
|
||||
if (
|
||||
!user.hasPermission(Permission.MANAGE_USERS) &&
|
||||
req.user?.id !== user.id
|
||||
) {
|
||||
user.movieQuotaDays = req.body.movieQuotaDays;
|
||||
user.movieQuotaLimit = req.body.movieQuotaLimit;
|
||||
user.tvQuotaDays = req.body.tvQuotaDays;
|
||||
user.tvQuotaLimit = req.body.tvQuotaLimit;
|
||||
}
|
||||
|
||||
if (!user.settings) {
|
||||
user.settings = new UserSettings({
|
||||
user: req.user,
|
||||
|
Reference in New Issue
Block a user