From 464b24fa91cf32d50f3eed0ba5def844948ae21b Mon Sep 17 00:00:00 2001 From: David Ludwig Date: Mon, 19 Apr 2021 18:43:10 -0500 Subject: [PATCH] Add CLI scripts. Move database connection code. Add movie quotas. Clean up models and routes --- .env.example | 6 +++ index.html | 2 +- package.json | 1 + src/cli/create_token.ts | 10 +++++ src/server/database/entities/MovieQuota.ts | 11 ++++++ src/server/database/entities/MovieTicket.ts | 29 ++++++++++---- src/server/database/entities/User.ts | 28 ++++++++++++- src/server/database/entities/index.ts | 1 + src/server/database/index.ts | 22 +++++++++++ src/server/services/Database.ts | 19 +-------- src/server/services/WebServer/routes/api.ts | 41 ++++++++++---------- src/server/services/WebServer/routes/auth.ts | 11 +++--- 12 files changed, 128 insertions(+), 53 deletions(-) create mode 100644 src/cli/create_token.ts create mode 100644 src/server/database/entities/MovieQuota.ts create mode 100644 src/server/database/index.ts diff --git a/.env.example b/.env.example index a4463c7..9b4b1ea 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,6 @@ +# The base URL the site is hosted on +BASE_URL = "https://autoplex.dlii.tech" + # Keys --------------------------------------------------------------------------------------------- # Application key to sign stuff @@ -25,6 +28,9 @@ DB_DATABASE = autoplex_request # Interfaces --------------------------------------------------------------------------------------- +# Seeker IPC socket path +SEEKER_IPC_SOCKET = /tmp/seeker.sock + # Torrent client IPC socket path TORRENT_CLIENT_IPC_SOCKET = /tmp/torrent_client.sock diff --git a/index.html b/index.html index 05a93c1..81c3e00 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - Autoplex Torrent + Autoplex
diff --git a/package.json b/package.json index a81e2f4..bcee839 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "main": "./build/server/index.js", "scripts": { "clean": "rimraf ./build", + "cli:token": "node -r ts-node/register -r tsconfig-paths/register ./src/cli/create_token.ts", "dev": "vite", "build": "yarn run build:backend && yarn run build:frontend", "build:backend": "ttsc -P ./tsconfig.server.json", diff --git a/src/cli/create_token.ts b/src/cli/create_token.ts new file mode 100644 index 0000000..41261a7 --- /dev/null +++ b/src/cli/create_token.ts @@ -0,0 +1,10 @@ +import connectToDatabase from "@server/database"; +import { RegisterToken } from "@server/database/entities"; +import { env } from "@server/util"; + +(async () => { + await connectToDatabase(); + let token = await RegisterToken.generate() + console.log(`${env("BASE_URL")}/register?token=${token.token}`); + process.exit(0); +})(); diff --git a/src/server/database/entities/MovieQuota.ts b/src/server/database/entities/MovieQuota.ts new file mode 100644 index 0000000..edba67b --- /dev/null +++ b/src/server/database/entities/MovieQuota.ts @@ -0,0 +1,11 @@ +import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm"; + +@Entity() +export class MovieQuota extends BaseEntity +{ + @PrimaryGeneratedColumn() + id!: number; + + @Column({ default: 5 }) + moviesPerWeek!: number; +} diff --git a/src/server/database/entities/MovieTicket.ts b/src/server/database/entities/MovieTicket.ts index 24db1ec..cfb8aeb 100644 --- a/src/server/database/entities/MovieTicket.ts +++ b/src/server/database/entities/MovieTicket.ts @@ -1,5 +1,5 @@ import { IApiMovieDetails } from "@common/api_schema"; -import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, OneToMany, OneToOne, JoinColumn } from "typeorm"; +import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, ManyToOne, OneToMany, OneToOne, JoinColumn, CreateDateColumn } from "typeorm"; import { MovieInfo } from "./MovieInfo"; import { MovieTorrent } from "./MovieTorrent"; import { User } from "./User"; @@ -10,14 +10,26 @@ export class MovieTicket extends BaseEntity @PrimaryGeneratedColumn() id!: number; - @Column({ type: "varchar", length: 27, unique: true, nullable: true }) + @Column({ type: "varchar", length: 27, nullable: true }) imdbId!: string | null; - @Column({ type: "varchar", nullable: true }) - title!: string | null; + @Column({ type: "varchar" }) + title!: string; - @Column({ type: "year", nullable: true }) - year!: number | null; + @Column({ type: "year" }) + year!: number; + + @CreateDateColumn() + createdAt!: Date; + + @Column({ default: false }) + isFulfilled!: boolean; + + @Column({ default: false }) + isCanceled!: boolean; + + @Column({ default: false }) + isStale!: boolean; @ManyToOne(() => User, user => user.movieTickets) user!: User; @@ -27,15 +39,16 @@ export class MovieTicket extends BaseEntity @OneToOne(() => MovieInfo, { nullable: true }) @JoinColumn() - info!: MovieInfo; + info!: MovieInfo | null; /** * Insert a request via IMDb movie details */ - public static async requestImdb(user: User, imdbId: string, title: string | null = null) { + public static async requestImdb(user: User, imdbId: string, title: string, year: number) { let ticket = new MovieTicket(); ticket.imdbId = imdbId; ticket.title = title; + ticket.year = year; ticket.user = user; return await ticket.save(); } diff --git a/src/server/database/entities/User.ts b/src/server/database/entities/User.ts index 6f68d74..7ee702c 100644 --- a/src/server/database/entities/User.ts +++ b/src/server/database/entities/User.ts @@ -1,8 +1,9 @@ -import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany } from "typeorm"; +import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, OneToMany, OneToOne, JoinColumn, CreateDateColumn } from "typeorm"; import bcrypt from "bcrypt"; import jwt from "jsonwebtoken"; import { MovieTicket } from "./MovieTicket"; import Application from "@server/Application"; +import { MovieQuota } from "./MovieQuota"; @Entity() export class User extends BaseEntity @@ -22,6 +23,13 @@ export class User extends BaseEntity @Column({ type: "char", length: 60 }) password!: string; + @CreateDateColumn() + createdAt!: Date; + + @OneToOne(() => MovieQuota, { nullable: true }) + @JoinColumn() + quota!: MovieQuota; + @OneToMany(() => User, user => user.movieTickets) movieTickets!: MovieTicket[]; @@ -43,4 +51,22 @@ export class User extends BaseEntity let body = { id: this.id, name: this.name, isAdmin: this.isAdmin }; return jwt.sign(body, key, { expiresIn }); } + + /** + * Create a new user + */ + public static async createUser(name: string, email: string, password: string, quota: number|null = 5) { + let user = new User(); + user.isAdmin = false; + user.name = name; + user.email = email; + user.password = await bcrypt.hash(password, 8); + // Create a quota if necessary + if (quota !== null) { + user.quota = new MovieQuota; + user.quota.moviesPerWeek = quota; + await user.quota.save(); + } + return await user.save(); + } } diff --git a/src/server/database/entities/index.ts b/src/server/database/entities/index.ts index b85cb59..2b8c4c9 100644 --- a/src/server/database/entities/index.ts +++ b/src/server/database/entities/index.ts @@ -1,4 +1,5 @@ export * from "./MovieInfo"; +export * from "./MovieQuota"; export * from "./MovieTicket"; export * from "./MovieTorrent"; export * from "./RegisterToken"; diff --git a/src/server/database/index.ts b/src/server/database/index.ts new file mode 100644 index 0000000..bfe0be5 --- /dev/null +++ b/src/server/database/index.ts @@ -0,0 +1,22 @@ +import * as entities from "./entities"; +import { readFile } from "fs/promises"; +import { createConnection } from "typeorm"; + +export default async function connectToDatabase() { + // Fetch the database password from the secret file + let password = (await readFile(process.env["DB_PASSWORD_FILE"])).toString().trim(); + + // Create the database connection + await createConnection({ + type : <"mysql" | "mariadb">process.env["DB_TYPE"], + host : process.env["DB_HOST"], + port : parseInt(process.env["DB_PORT"]), + username : process.env["DB_USER"], + password : password, + database : process.env["DB_DATABASE"], + // synchronize: process.env["NODE_ENV"] != "production", + synchronize: true, // Seems stable enough for my liking + entities : Object.values(entities), + migrations : ["src/migrations/*.ts"] + }); +} diff --git a/src/server/services/Database.ts b/src/server/services/Database.ts index baf2d0d..bcdea60 100644 --- a/src/server/services/Database.ts +++ b/src/server/services/Database.ts @@ -1,8 +1,8 @@ import { Connection, createConnection } from "typeorm"; import * as entities from "../database/entities"; import Service from "./Service"; -import { readFile } from "fs/promises"; import Application from "../Application"; +import connectToDatabase from "@server/database"; export default class Database extends Service { @@ -22,22 +22,7 @@ export default class Database extends Service * Boot the database service */ public async boot() { - // Fetch the database password from the secret file - let password = (await readFile(process.env["DB_PASSWORD_FILE"])).toString().trim(); - - // Create the database connection - await createConnection({ - type : <"mysql" | "mariadb">process.env["DB_TYPE"], - host : process.env["DB_HOST"], - port : parseInt(process.env["DB_PORT"]), - username : process.env["DB_USER"], - password : password, - database : process.env["DB_DATABASE"], - // synchronize: process.env["NODE_ENV"] != "production", - synchronize: true, // Seems stable enough for my liking - entities : Object.values(entities), - migrations : ["src/migrations/*.ts"] - }); + await connectToDatabase(); } /** diff --git a/src/server/services/WebServer/routes/api.ts b/src/server/services/WebServer/routes/api.ts index 8fa8f1f..2a4f254 100644 --- a/src/server/services/WebServer/routes/api.ts +++ b/src/server/services/WebServer/routes/api.ts @@ -73,26 +73,27 @@ export default function register(factory: RouteRegisterFactory, app: Application /** * Request a movie to download */ - factory.get("/create/imdb/:imdb_id", handle([RequestImdbMovieRequest], async (request, reply) => { - // Verify that the ID has not yet been requested - let imdbId = (request.params)["imdb_id"]; - let title = (request.query)["title"] || null; - if (0 != await MovieTicket.count({imdbId})) { - reply.status(409); - reply.send({ status: "Conflict" }); - return; - } - // Verify that the IMDb ID exists - if (!await app.service("Movie Search").verifyImdbId(imdbId)) { - reply.status(404); - reply.send({ satus: "Not found" }); - return; - } - // Create the movie request ticket - let user = request.middlewareParams.auth.user; - let ticket = await MovieTicket.requestImdb(user, imdbId, title); - return reply.send({ status: "Success", data: { ticket_id: ticket.id }}); - })); + // factory.get("/create/imdb/:imdb_id", handle([RequestImdbMovieRequest], async (request, reply) => { + // // Verify that the ID has not yet been requested + // let imdbId = (request.params)["imdb_id"]; + // let title = (request.query)["title"] || null; + // // let year = // Scrape IMDb to find out... + // if (0 != await MovieTicket.count({imdbId})) { + // reply.status(409); + // reply.send({ status: "Conflict" }); + // return; + // } + // // Verify that the IMDb ID exists + // if (!await app.service("Movie Search").verifyImdbId(imdbId)) { + // reply.status(404); + // reply.send({ satus: "Not found" }); + // return; + // } + // // Create the movie request ticket + // let user = request.middlewareParams.auth.user; + // // let ticket = await MovieTicket.requestImdb(user, imdbId, title, year); + // return reply.send({ status: "Success", data: { ticket_id: ticket.id }}); + // })); /** * Remove/cancel a request diff --git a/src/server/services/WebServer/routes/auth.ts b/src/server/services/WebServer/routes/auth.ts index a272099..39fcc95 100644 --- a/src/server/services/WebServer/routes/auth.ts +++ b/src/server/services/WebServer/routes/auth.ts @@ -48,12 +48,11 @@ export default function register(factory: RouteRegisterFactory, app: Application */ factory.post("/auth/register", handle([RegisterRequest], async (request, reply) => { let body = request.body; - let user = new User(); - user.isAdmin = false; - user.name = body.name.trim(); - user.email = body.email.trim(); - user.password = await bcrypt.hash(body.password, 8); - await user.save(); + await User.createUser( + body.name.trim(), + body.email.trim(), + body.password.trim() + ); await RegisterToken.delete({token: body.token }); reply.send({ status: "success" }); }));