feat(api): request api (#80)

This commit is contained in:
sct
2020-09-13 18:55:35 +09:00
committed by GitHub
parent b600671acc
commit f4c2c47e56
8 changed files with 274 additions and 13 deletions

View File

@@ -99,7 +99,7 @@ interface TmdbSearchTvResponse extends TmdbPaginatedResponse {
results: TmdbTvResult[]; results: TmdbTvResult[];
} }
interface TmdbMovieDetails { export interface TmdbMovieDetails {
id: number; id: number;
imdb_id?: string; imdb_id?: string;
adult: boolean; adult: boolean;
@@ -154,7 +154,7 @@ interface TmdbTvEpisodeDetails {
vote_cuont: number; vote_cuont: number;
} }
interface TmdbTvDetails { export interface TmdbTvDetails {
id: number; id: number;
backdrop_path?: string; backdrop_path?: string;
created_by: { created_by: {

View File

@@ -7,10 +7,11 @@ import {
UpdateDateColumn, UpdateDateColumn,
getRepository, getRepository,
In, In,
Index,
} from 'typeorm'; } from 'typeorm';
import { User } from './User'; import { User } from './User';
export enum Status { export enum MediaRequestStatus {
PENDING, PENDING,
APPROVED, APPROVED,
DECLINED, DECLINED,
@@ -44,14 +45,15 @@ export class MediaRequest {
@PrimaryGeneratedColumn() @PrimaryGeneratedColumn()
public id: number; public id: number;
@Column() @Column({ unique: true })
@Index()
public mediaId: number; public mediaId: number;
@Column() @Column()
public mediaType: 'movie' | 'tv'; public mediaType: 'movie' | 'tv';
@Column({ type: 'integer' }) @Column({ type: 'integer' })
public status: Status; public status: MediaRequestStatus;
@ManyToOne(() => User, (user) => user.requests, { eager: true }) @ManyToOne(() => User, (user) => user.requests, { eager: true })
public requestedBy: User; public requestedBy: User;
@@ -65,7 +67,7 @@ export class MediaRequest {
@UpdateDateColumn() @UpdateDateColumn()
public updatedAt: Date; public updatedAt: Date;
constructor(init?: Partial<User>) { constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init); Object.assign(this, init);
} }
} }

View File

@@ -6,6 +6,7 @@ export enum Permission {
MANAGE_REQUESTS = 16, MANAGE_REQUESTS = 16,
REQUEST = 32, REQUEST = 32,
VOTE = 64, VOTE = 64,
AUTO_APPROVE = 128,
} }
/** /**

View File

@@ -3,7 +3,7 @@ import type {
TmdbPersonResult, TmdbPersonResult,
TmdbTvResult, TmdbTvResult,
} from '../api/themoviedb'; } from '../api/themoviedb';
import { MediaRequest } from '../entity/MediaRequest'; import type { MediaRequest } from '../entity/MediaRequest';
export type MediaType = 'tv' | 'movie' | 'person'; export type MediaType = 'tv' | 'movie' | 'person';

View File

@@ -356,8 +356,10 @@ components:
$ref: '#/components/schemas/User' $ref: '#/components/schemas/User'
readOnly: true readOnly: true
modifiedBy: modifiedBy:
$ref: '#/components/schemas/User' anyOf:
readOnly: true - $ref: '#/components/schemas/User'
- type: string
nullable: true
required: required:
- id - id
- mediaId - mediaId
@@ -872,6 +874,123 @@ paths:
type: array type: array
items: items:
$ref: '#/components/schemas/TvResult' $ref: '#/components/schemas/TvResult'
/request:
get:
summary: Get all requests
description: |
Returns all requests if the user has the `ADMIN` or `MANAGE_REQUESTS` permissions. Otherwise, only the logged in users requests are returned.
tags:
- request
responses:
'200':
description: Requests returned
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/MediaRequest'
post:
summary: Create a new request
description: |
Creates a new request with the provided media id and type. The `REQUEST` permission is required.
If the user has the `ADMIN` or `AUTO_APPROVE` permissions, their request will be auomatically approved.
tags:
- request
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
mediaType:
type: string
enum: [movie, tv]
example: movie
mediaId:
type: number
example: 123
responses:
'201':
description: Succesfully created the request
content:
application/json:
schema:
$ref: '#/components/schemas/MediaRequest'
/request/{requestId}:
get:
summary: Requests a specific MediaRequest
description: Returns a MediaRequest in JSON format
tags:
- request
parameters:
- in: path
name: requestId
description: Request ID
required: true
example: 1
schema:
type: string
responses:
'200':
description: Succesfully returns request
content:
application/json:
schema:
$ref: '#/components/schemas/MediaRequest'
delete:
summary: Delete a request
description: Removes a request. If the user has the `MANAGE_REQUESTS` permission, then any request can be removed. Otherwise, only pending requests can be removed.
tags:
- request
parameters:
- in: path
name: requestId
description: Request ID
required: true
example: 1
schema:
type: string
responses:
'200':
description: Succesfully removed request
content:
application/json:
schema:
$ref: '#/components/schemas/MediaRequest'
/request/{requestId}/{status}:
get:
summary: Update a requests status
description: |
Updates a requests status to approved or declined. Also returns the request in JSON format
Requires the `MANAGE_REQUESTS` permission or `ADMIN`
tags:
- request
parameters:
- in: path
name: requestId
description: Request ID
required: true
schema:
type: string
example: 1
- in: path
name: status
description: New status
required: true
schema:
type: string
enum: [pending, approve, decline, available]
responses:
'200':
description: Request status changed
content:
application/json:
schema:
$ref: '#/components/schemas/MediaRequest'
security: security:
- cookieAuth: [] - cookieAuth: []

View File

@@ -7,6 +7,7 @@ import { Permission } from '../lib/permissions';
import { getSettings } from '../lib/settings'; import { getSettings } from '../lib/settings';
import searchRoutes from './search'; import searchRoutes from './search';
import discoverRoutes from './discover'; import discoverRoutes from './discover';
import requestRoutes from './request';
const router = Router(); const router = Router();
@@ -19,6 +20,7 @@ router.use(
); );
router.use('/search', isAuthenticated(), searchRoutes); router.use('/search', isAuthenticated(), searchRoutes);
router.use('/discover', isAuthenticated(), discoverRoutes); router.use('/discover', isAuthenticated(), discoverRoutes);
router.use('/request', isAuthenticated(), requestRoutes);
router.use('/auth', authRoutes); router.use('/auth', authRoutes);
router.get('/settings/public', (_req, res) => { router.get('/settings/public', (_req, res) => {

140
server/routes/request.ts Normal file
View File

@@ -0,0 +1,140 @@
import { Router } from 'express';
import { isAuthenticated } from '../middleware/auth';
import { Permission } from '../lib/permissions';
import { getRepository } from 'typeorm';
import { MediaRequest, MediaRequestStatus } from '../entity/MediaRequest';
import TheMovieDb from '../api/themoviedb';
const requestRoutes = Router();
requestRoutes.get('/', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const requests = req.user?.hasPermission(Permission.MANAGE_REQUESTS)
? await requestRepository.find()
: await requestRepository.find({
where: { requestedBy: { id: req.user?.id } },
});
return res.status(200).json(requests);
} catch (e) {
next({ status: 500, message: e.message });
}
});
requestRoutes.post(
'/',
isAuthenticated(Permission.REQUEST),
async (req, res, next) => {
const tmdb = new TheMovieDb();
const requestRepository = getRepository(MediaRequest);
try {
const media =
req.body.mediaType === 'movie'
? await tmdb.getMovie({ movieId: req.body.mediaId })
: await tmdb.getTvShow({ tvId: req.body.mediaId });
const request = new MediaRequest({
mediaId: media.id,
mediaType: req.body.mediaType,
requestedBy: req.user,
// If the user is an admin or has the "auto approve" permission, automatically approve the request
status: req.user?.hasPermission(Permission.AUTO_APPROVE)
? MediaRequestStatus.APPROVED
: MediaRequestStatus.PENDING,
});
await requestRepository.save(request);
return res.status(201).json(request);
} catch (e) {
next({ message: e.message, status: 500 });
}
}
);
requestRoutes.get('/:requestId', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOneOrFail({
where: { id: Number(req.params.requestId) },
relations: ['requestedBy', 'modifiedBy'],
});
return res.status(200).json(request);
} catch (e) {
next({ status: 404, message: 'Request not found' });
}
});
requestRoutes.delete('/:requestId', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOneOrFail({
where: { id: Number(req.params.requestId) },
relations: ['requestedBy', 'modifiedBy'],
});
if (
!req.user?.hasPermission(Permission.MANAGE_REQUESTS) &&
(request.requestedBy.id !== req.user?.id || request.status > 0)
) {
return next({
status: 401,
message: 'You do not have permission to remove this request',
});
}
requestRepository.delete(request.id);
return res.status(200).json(request);
} catch (e) {
next({ status: 404, message: 'Request not found' });
}
});
requestRoutes.get<{
requestId: string;
status: 'pending' | 'approve' | 'decline' | 'available';
}>(
'/:requestId/:status',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOneOrFail({
where: { id: Number(req.params.requestId) },
relations: ['requestedBy', 'modifiedBy'],
});
let newStatus: MediaRequestStatus;
switch (req.params.status) {
case 'pending':
newStatus = MediaRequestStatus.PENDING;
break;
case 'approve':
newStatus = MediaRequestStatus.APPROVED;
break;
case 'decline':
newStatus = MediaRequestStatus.DECLINED;
break;
case 'available':
newStatus = MediaRequestStatus.AVAILABLE;
break;
}
request.status = newStatus;
await requestRepository.save(request);
return res.status(200).json(request);
} catch (e) {
next({ status: 404, message: 'Request not found' });
}
}
);
export default requestRoutes;

View File

@@ -3,10 +3,7 @@
"compilerOptions": { "compilerOptions": {
"module": "commonjs", "module": "commonjs",
"outDir": "../dist", "outDir": "../dist",
"noEmit": false, "noEmit": false
"strictPropertyInitialization": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}, },
"include": ["**/*.ts"] "include": ["**/*.ts"]
} }