Browse Source

Convert the request service to use the new webserver package

dev
David Ludwig 4 years ago
parent
commit
abf2548989
15 changed files with 56 additions and 321 deletions
  1. +1
    -0
      services/request/package.json
  2. +12
    -99
      services/request/src/server/services/WebServer/index.ts
  3. +3
    -3
      services/request/src/server/services/WebServer/middleware/auth.ts
  4. +2
    -2
      services/request/src/server/services/WebServer/requests/LinkDiscordRequest.ts
  5. +2
    -2
      services/request/src/server/services/WebServer/requests/LoginRequest.ts
  6. +2
    -2
      services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts
  7. +2
    -2
      services/request/src/server/services/WebServer/requests/RegisterRequest.ts
  8. +0
    -57
      services/request/src/server/services/WebServer/requests/Request.ts
  9. +2
    -2
      services/request/src/server/services/WebServer/requests/RequestImdbMovieRequest.ts
  10. +2
    -2
      services/request/src/server/services/WebServer/requests/RequestMovieRequest.ts
  11. +2
    -2
      services/request/src/server/services/WebServer/requests/RequestTmdbMovieRequest.ts
  12. +7
    -23
      services/request/src/server/services/WebServer/requests/index.ts
  13. +0
    -108
      services/request/src/server/services/WebServer/routes/RouteRegisterFactory.ts
  14. +10
    -7
      services/request/src/server/services/WebServer/routes/api.ts
  15. +9
    -10
      services/request/src/server/services/WebServer/routes/auth.ts

+ 1
- 0
services/request/package.json View File

@ -19,6 +19,7 @@
"@autoplex/database": "^0.0.0",
"@autoplex/microservice": "^0.0.0",
"@autoplex/utils": "^0.0.0",
"@autoplex/webserver": "^0.0.0",
"@fortawesome/fontawesome-free": "^5.15.3",
"bcrypt": "^5.0.1",
"discord.js": "^12.5.3",


+ 12
- 99
services/request/src/server/services/WebServer/index.ts View File

@ -1,121 +1,34 @@
import fastify from "fastify";
import fastifyCookie from "fastify-cookie";
import fastifyFormBody from "fastify-formbody";
import fastifyHttpProxy from "fastify-http-proxy";
import fastifyMultipart from "fastify-multipart";
import fastifyStatic from "fastify-static";
import Application from "@server/Application";
import { InternalService } from "@autoplex/microservice";
import { join } from "path";
import routes from "./routes";
import "./validators";
import RouteRegisterFactory, { RouteFactory } from "./routes/RouteRegisterFactory";
import { WebServerService } from "@autoplex/webserver";
import { env } from "@autoplex/utils";
export default class WebServer extends InternalService<Application>
export default class WebServer extends WebServerService<Application>
{
/**
* The port to host the webserver on
*/
protected readonly port: number;
/**
* The internal webserver instance
*/
protected fastify: ReturnType<typeof fastify>;
/**
* Create a new webserver instance
*/
public constructor(app: Application) {
super(app);
this.port = parseInt(<string>process.env["WEBSERVER_PORT"]);
this.fastify = fastify();
}
/**
* The service name
*/
public readonly NAME = "Web Server";
/**
* Register required Fastify plugins
*/
protected registerPlugins() {
return Promise.all([
this.fastify.register(fastifyCookie, {
secret: this.app.APP_KEY
}),
this.fastify.register(fastifyFormBody),
this.fastify.register(fastifyMultipart, {
limits: {
fileSize: 16*1024*1024,
files: 50
}
})
]);
}
/**
* Boot the webserver
*/
public async boot() {
// Install plugins
await this.registerPlugins();
// Register the routes
this.registerRoutes();
}
/**
* Start the webserver
* The port to host the webserver on
*/
public start() {
// Start listening
this.fastify.listen(this.port, "0.0.0.0");
this.log("Webserver listening on port:", this.port);
}
protected readonly PORT: number = parseInt(env("WEBSERVER_PORT"));
/**
* Shutdown the webserver
* The application key
*/
public async shutdown() {
this.log("Webserver shutting down");
await this.fastify.close();
}
// ---------------------------------------------------------------------------------------------
protected readonly APP_KEY = this.app.APP_KEY;
/**
* Register all route groups
* @TODO
* Register SPA routes for now
*/
protected registerRoutes() {
this.registerSpaRoutes();
let factory = new RouteRegisterFactory(this, this.fastify, this.app);
for (let group in routes) {
<RouteFactory>(<any>routes)[group](factory, this.app);
}
}
protected REGISTER_SPA_ROUTES = true;
/**
* Register the routes required for the single-page application
* The routes to register
*/
protected registerSpaRoutes() {
/**
* If the app is in production mode, serve static assets.
* If the app is in development mode, forward 404's to Vite.
*/
if (process.env["NODE_ENV"] == "production") {
this.fastify.register(fastifyStatic, {
root: join(__dirname, "../../../public")
});
this.fastify.setNotFoundHandler((request, reply) => {
return reply.sendFile("index.html");
});
} else {
this.log("Using Vite proxy");
this.fastify.register(fastifyHttpProxy, {
upstream: "http://localhost:3001"
});
}
}
protected ROUTES = Object.values(routes);
}

+ 3
- 3
services/request/src/server/services/WebServer/middleware/auth.ts View File

@ -1,14 +1,14 @@
import { FastifyReply, FastifyRequest } from "fastify";
import { IteratorNext, MiddlewareRequest } from "@autoplex/webserver";
import { FastifyReply } from "fastify";
import Application from "@server/Application";
import jwt from "jsonwebtoken";
import { IteratorNext, MiddlewareRequest } from ".";
import { ITokenSchema } from "@common/api_schema";
import { User } from "@autoplex/database";
/**
* Attempt to authenticate a client's JWT token
*/
async function authenticateJwtToken(request: FastifyRequest, reply: FastifyReply): Promise<User|undefined> {
async function authenticateJwtToken<T>(request: MiddlewareRequest<T>, reply: FastifyReply): Promise<User|undefined> {
// Verify headers
if (!request.headers["authorization"]) {
reply.status(401);


+ 2
- 2
services/request/src/server/services/WebServer/requests/LinkDiscordRequest.ts View File

@ -1,7 +1,7 @@
import { FastifyRequest } from "fastify";
import LoginRequest from "./LoginRequest";
import { LoginRequest } from "./LoginRequest";
export default class LinkDiscordRequest extends LoginRequest
export class LinkDiscordRequest extends LoginRequest
{
public checkFormat(request: FastifyRequest) {
if ((<any>request.params)["token"] === undefined) {


+ 2
- 2
services/request/src/server/services/WebServer/requests/LoginRequest.ts View File

@ -1,14 +1,14 @@
import { FastifyRequest } from "fastify";
import validate from "validate.js";
import { constraints } from "@common/validation";
import Request from "./Request";
import { Request } from "@autoplex/webserver";
export interface ILoginFormBody {
email: string,
password: string
}
export default class LoginRequest extends Request
export class LoginRequest extends Request
{
/**
* Validate the request


+ 2
- 2
services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts View File

@ -1,9 +1,9 @@
import { constraints } from "@common/validation";
import { FastifyRequest } from "fastify";
import validate from "validate.js";
import Request from "./Request";
import { Request } from "@autoplex/webserver";
export default class MovieSearchRequest extends Request
export class MovieSearchRequest extends Request
{
public validate(request: FastifyRequest) {
return validate.async(request.query, {


+ 2
- 2
services/request/src/server/services/WebServer/requests/RegisterRequest.ts View File

@ -1,7 +1,7 @@
import { FastifyRequest } from "fastify";
import validate from "validate.js";
import { constraints } from "@common/validation";
import Request from "./Request";
import { Request } from "@autoplex/webserver";
export interface IRegisterFormBody {
token: string,
@ -11,7 +11,7 @@ export interface IRegisterFormBody {
retypePassword: string
}
export default class RegisterRequest extends Request
export class RegisterRequest extends Request
{
/**
* Validate the request


+ 0
- 57
services/request/src/server/services/WebServer/requests/Request.ts View File

@ -1,57 +0,0 @@
import { FastifyRequest, FastifyReply } from "fastify";
import { MiddlewareRequest } from "../middleware";
export default class Request<T = unknown>
{
/**
* Handle the incoming request
*/
public async handle(request: MiddlewareRequest<T>, reply: FastifyReply) {
if (!this.checkFormat(request)) {
reply.status(400);
return {
status: "Bad request"
};
}
if (!await this.isAuthorized(request)) {
reply.status(403);
return {
status: "Forbidden"
};
}
try {
await this.validate(request);
} catch(errors) {
reply.status(422);
return {
status: "Unprocessable entities",
errors
};
}
}
// Overridable ---------------------------------------------------------------------------------
/**
* Check the format of the given request
*/
public checkFormat(request: MiddlewareRequest<T>) {
return true;
}
/**
* Check if the user is authorized to make this request
*/
public async isAuthorized(request: MiddlewareRequest<T>) {
return true;
}
/**
* Validate the request and return any errors
*/
public async validate(request: MiddlewareRequest<T>): Promise<any|undefined> {
return undefined;
}
}
Request.apply(undefined);

+ 2
- 2
services/request/src/server/services/WebServer/requests/RequestImdbMovieRequest.ts View File

@ -1,9 +1,9 @@
import validate from "validate.js";
import { MiddlewareRequest } from "../middleware";
import { IAuthMiddlewareParams } from "../middleware/auth";
import RequestMovieRequest from "./RequestMovieRequest";
import { RequestMovieRequest } from "./RequestMovieRequest";
export default class RequestImdbMovieRequest<T extends IAuthMiddlewareParams> extends RequestMovieRequest<T>
export class RequestImdbMovieRequest<T extends IAuthMiddlewareParams> extends RequestMovieRequest<T>
{
public validate(request: MiddlewareRequest<T>) {
return validate.async(request.params, {


+ 2
- 2
services/request/src/server/services/WebServer/requests/RequestMovieRequest.ts View File

@ -1,8 +1,8 @@
import { MiddlewareRequest } from "../middleware";
import { IAuthMiddlewareParams } from "../middleware/auth";
import Request from "./Request";
import { Request } from "@autoplex/webserver";
export default class RequestMovieRequest<T extends IAuthMiddlewareParams> extends Request
export class RequestMovieRequest<T extends IAuthMiddlewareParams> extends Request<T>
{
/**
* Ensure the user is able to request movies


+ 2
- 2
services/request/src/server/services/WebServer/requests/RequestTmdbMovieRequest.ts View File

@ -1,9 +1,9 @@
import { FastifyRequest } from "fastify";
import validate from "validate.js";
import { IAuthMiddlewareParams } from "../middleware/auth";
import RequestMovieRequest from "./RequestMovieRequest";
import { RequestMovieRequest } from "./RequestMovieRequest";
export default class RequestTmdbMovieRequest<T extends IAuthMiddlewareParams> extends RequestMovieRequest<T>
export class RequestTmdbMovieRequest<T extends IAuthMiddlewareParams> extends RequestMovieRequest<T>
{
public validate(request: FastifyRequest) {
return validate.async(request.params, {


+ 7
- 23
services/request/src/server/services/WebServer/requests/index.ts View File

@ -1,23 +1,7 @@
import Request from "./Request";
import { MiddlewareRequest } from "../middleware";
import { FastifyReply } from "fastify";
type RequestConstructor<T> = new () => Request<T>;
export type RouteMiddlewareHandler<T> = (request: MiddlewareRequest<T>, reply: FastifyReply) => Promise<void> | void;
export default function handle<T>(requests: RequestConstructor<T>[], handle: RouteMiddlewareHandler<T>): RouteMiddlewareHandler<T>
{
return async (fastifyRequest, fastifyReply) => {
// Request parsing
for (let requestClass of requests) {
let request = new requestClass();
let response = await request.handle(fastifyRequest, fastifyReply);
if (response) {
fastifyReply.send(response);
return;
}
}
// Requests have been parsed/handled successfully, proceed to process the request
await (<any>handle)(fastifyRequest, fastifyReply);
}
}
export * from "./LinkDiscordRequest";
export * from "./LoginRequest";
export * from "./MovieSearchRequest";
export * from "./RegisterRequest";
export * from "./RequestImdbMovieRequest";
export * from "./RequestMovieRequest";
export * from "./RequestTmdbMovieRequest";

+ 0
- 108
services/request/src/server/services/WebServer/routes/RouteRegisterFactory.ts View File

@ -1,108 +0,0 @@
import { FastifyInstance } from "fastify";
import Application from "@server/Application";
import { handleMiddleware, HandlerMethodWithMiddleware, MiddlewareMethod } from "../middleware";
import fastifyHttpProxy from "fastify-http-proxy";
import WebServer from "..";
export type RouteFactory<M extends MiddlewareMethod<any> = never> = ((factory: RouteRegisterFactory<M>) => void)
| ((factory: RouteRegisterFactory<M>, app: Application) => void);
export default class RouteRegisterFactory<M extends MiddlewareMethod<any> = MiddlewareMethod<void>>
{
/**
* The application instance
*/
protected readonly app: Application;
/**
* The Fastify server instance
*/
protected readonly fastify: FastifyInstance;
/**
* The webserver instance
*/
protected readonly webserver: WebServer;
/**
* The list of middleware
*/
protected middleware: M[] = [];
/**
* The current route prefix
*/
protected pathPrefix: string = "";
/**
* Create a new route factory
*/
public constructor(webserver: WebServer, fastify: FastifyInstance, app: Application) {
this.app = app;
this.fastify = fastify;
this.webserver = webserver;
}
/**
* Register a group of routes under a common prefix and middleware
*/
public prefix<T extends MiddlewareMethod<any>>(prefix: string, middleware: T[], factory: RouteFactory<(M|T)>) {
let prefixBackup = this.pathPrefix;
this.pathPrefix += prefix;
this.group(middleware, factory);
this.pathPrefix = prefixBackup;
}
/**
* Register a group of routes under common middleware
*/
public group<T extends MiddlewareMethod<any>>(middleware: T[], factory: RouteFactory<(M|T)>) {
let middlewareBackup = this.middleware;
this.middleware = this.middleware.concat(<any>middleware);
factory(<RouteRegisterFactory<(M|T)>>this, this.app);
this.middleware = middlewareBackup;
}
/**
* Register a GET request
*/
public get<T extends MiddlewareMethod<any>>(path: string, handler: HandlerMethodWithMiddleware<M[]>): void;
public get<T extends MiddlewareMethod<any>>(path: string, middleware: T[], handler: HandlerMethodWithMiddleware<(T|M)[]>): void;
public get<T extends MiddlewareMethod<any>>(path: string, middleware: HandlerMethodWithMiddleware<(T|M)[]>|T[], handler?: HandlerMethodWithMiddleware<(T|M)[]>) {
type Handler = HandlerMethodWithMiddleware<(T|M)[]>;
handler = (handler ?? <Handler>middleware);
middleware = (middleware instanceof Array) ? <any>this.middleware.concat(<any>middleware) : this.middleware;
this.fastify.get(`${this.pathPrefix}${path}`, handleMiddleware(<(T|M)[]>middleware, <Handler>handler));
}
/**
* Register a POST request
*/
public post<T extends MiddlewareMethod<any>>(path: string, handler: HandlerMethodWithMiddleware<M[]>): void;
public post<T extends MiddlewareMethod<any>>(path: string, middleware: T[], handler: HandlerMethodWithMiddleware<(T|M)[]>): void;
public post<T extends MiddlewareMethod<any>>(path: string, middleware: HandlerMethodWithMiddleware<(T|M)[]>|T[], handler?: HandlerMethodWithMiddleware<(T|M)[]>) {
type Handler = HandlerMethodWithMiddleware<(T|M)[]>;
handler = (handler ?? <Handler>middleware);
middleware = (middleware instanceof Array) ? <any>this.middleware.concat(<any>middleware) : this.middleware;
this.fastify.post(`${this.pathPrefix}${path}`, handleMiddleware(<(T|M)[]>middleware, <Handler>handler));
}
/**
* Register a proxy route
*/
public proxy<T extends MiddlewareMethod<any>>(path: string, upstream: string, middleware?: T[]) {
this.log(`Registering proxy: ${this.pathPrefix}${path} -> ${upstream}`);
this.fastify.register(fastifyHttpProxy, {
prefix: `${this.pathPrefix}${path}`,
beforeHandler: middleware ? handleMiddleware(this.middleware.concat(<any>middleware)) : undefined,
upstream
});
}
/**
* Log under the WebServer service
*/
public log(...args: any[]) {
this.webserver.log(...args);
}
}

+ 10
- 7
services/request/src/server/services/WebServer/routes/api.ts View File

@ -1,16 +1,19 @@
import { MovieInfo, MovieTicket } from "@autoplex/database";
import { handleRequest, RouteRegisterFactory, RouteFactory, MiddlewareMethod } from "@autoplex/webserver";
import Application from "@server/Application";
import SeekerIpc from "@server/services/Ipc/SeekerIpcClient";
import MovieSearch from "@server/services/MovieSearch";
import { auth } from "../middleware/auth";
import RouteRegisterFactory from "./RouteRegisterFactory";
import handle from "../requests";
import { MovieInfo, MovieTicket } from "@autoplex/database";
import RequestTmdbMovieRequest from "../requests/RequestTmdbMovieRequest";
import { RequestTmdbMovieRequest } from "../requests";
function testFunc<A extends any = number, B extends any = string>(a: A, b: B) {
}
/**
* Register API routes
*/
export default function register(factory: RouteRegisterFactory, app: Application) {
export default function register(factory: RouteRegisterFactory<MiddlewareMethod<void>, Application>, app: Application) {
factory.prefix("/api", [auth], (factory, app) => {
@ -59,7 +62,7 @@ export default function register(factory: RouteRegisterFactory, app: Application
/**
* Request a movie to download
*/
factory.get("/create/tmdb/:tmdb_id", handle([RequestTmdbMovieRequest], async (request, reply) => {
factory.get("/create/tmdb/:tmdb_id", handleRequest([RequestTmdbMovieRequest], async (request, reply) => {
// Verify that the ID has not yet been requested
let tmdbId = (<any>request.params)["tmdb_id"];
if (0 != await MovieTicket.count({ where: { tmdbId, isCanceled: false } })) {
@ -101,7 +104,7 @@ export default function register(factory: RouteRegisterFactory, app: Application
/**
* Request a movie to download
*/
// factory.get("/create/imdb/:imdb_id", handle([RequestImdbMovieRequest], async (request, reply) => {
// factory.get("/create/imdb/:imdb_id", handleRequest([RequestImdbMovieRequest], async (request, reply) => {
// // Verify that the ID has not yet been requested
// let imdbId = (<any>request.params)["imdb_id"];
// let title = (<any>request.query)["title"] || null;


+ 9
- 10
services/request/src/server/services/WebServer/routes/auth.ts View File

@ -1,17 +1,16 @@
import { DiscordLinkRequest, RegisterToken, User } from "@autoplex/database";
import { RouteRegisterFactory, handleRequest, MiddlewareMethod } from "@autoplex/webserver";
import jwt from "jsonwebtoken"
import Application from "@server/Application";
import { DiscordLinkRequest, RegisterToken, User } from "@autoplex/database";
import LoginRequest, { ILoginFormBody } from "../requests/LoginRequest";
import RegisterRequest, { IRegisterFormBody } from "../requests/RegisterRequest";
import handle from "../requests";
import { LoginRequest, ILoginFormBody } from "../requests";
import { RegisterRequest, IRegisterFormBody } from "../requests";
import { LinkDiscordRequest } from "../requests";
import { auth } from "../middleware/auth";
import RouteRegisterFactory from "./RouteRegisterFactory";
import LinkDiscordRequest from "../requests/LinkDiscordRequest";
/**
* Register authentication routes
*/
export default function register(factory: RouteRegisterFactory, app: Application) {
export default function register(factory: RouteRegisterFactory<MiddlewareMethod<void>, Application>, app: Application) {
factory.get("/auth/verify", [auth], (request, reply) => {
console.log("Authentication has been verified");
@ -20,7 +19,7 @@ export default function register(factory: RouteRegisterFactory, app: Application
// Login ---------------------------------------------------------------------------------------
factory.post("/auth/login", handle([LoginRequest], async (request, reply) => {
factory.post("/auth/login", handleRequest([LoginRequest], async (request, reply) => {
let form = <ILoginFormBody>request.body;
let user = await User.authenticate(form.email, form.password);
if (user === null) {
@ -50,7 +49,7 @@ export default function register(factory: RouteRegisterFactory, app: Application
/**
* Register a user
*/
factory.post("/auth/register", handle([RegisterRequest], async (request, reply) => {
factory.post("/auth/register", handleRequest([RegisterRequest], async (request, reply) => {
let body = <IRegisterFormBody>request.body;
await User.createUser(
body.name.trim(),
@ -102,7 +101,7 @@ export default function register(factory: RouteRegisterFactory, app: Application
/**
* Link a Discord account
*/
factory.post("/auth/link/discord/:token", handle([LinkDiscordRequest], async (request, reply) => {
factory.post("/auth/link/discord/:token", handleRequest([LinkDiscordRequest], async (request, reply) => {
// Fetch the link request from the token
let token = <string|undefined>(<any>request.params)["token"];
let linkRequest = await DiscordLinkRequest.findOne({ where: { token } });


Loading…
Cancel
Save