diff --git a/services/request/package.json b/services/request/package.json index 083cf37..5a67777 100644 --- a/services/request/package.json +++ b/services/request/package.json @@ -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", diff --git a/services/request/src/server/services/WebServer/index.ts b/services/request/src/server/services/WebServer/index.ts index 4ddcad7..d7086ea 100644 --- a/services/request/src/server/services/WebServer/index.ts +++ b/services/request/src/server/services/WebServer/index.ts @@ -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 +export default class WebServer extends WebServerService { - /** - * The port to host the webserver on - */ - protected readonly port: number; - - /** - * The internal webserver instance - */ - protected fastify: ReturnType; - - /** - * Create a new webserver instance - */ - public constructor(app: Application) { - super(app); - this.port = parseInt(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) { - (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); } diff --git a/services/request/src/server/services/WebServer/middleware/auth.ts b/services/request/src/server/services/WebServer/middleware/auth.ts index 562930c..58b4781 100644 --- a/services/request/src/server/services/WebServer/middleware/auth.ts +++ b/services/request/src/server/services/WebServer/middleware/auth.ts @@ -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 { +async function authenticateJwtToken(request: MiddlewareRequest, reply: FastifyReply): Promise { // Verify headers if (!request.headers["authorization"]) { reply.status(401); diff --git a/services/request/src/server/services/WebServer/requests/LinkDiscordRequest.ts b/services/request/src/server/services/WebServer/requests/LinkDiscordRequest.ts index ad8852e..2c7b3e7 100644 --- a/services/request/src/server/services/WebServer/requests/LinkDiscordRequest.ts +++ b/services/request/src/server/services/WebServer/requests/LinkDiscordRequest.ts @@ -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 ((request.params)["token"] === undefined) { diff --git a/services/request/src/server/services/WebServer/requests/LoginRequest.ts b/services/request/src/server/services/WebServer/requests/LoginRequest.ts index 5e45e45..0c266ea 100644 --- a/services/request/src/server/services/WebServer/requests/LoginRequest.ts +++ b/services/request/src/server/services/WebServer/requests/LoginRequest.ts @@ -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 diff --git a/services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts b/services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts index 910d28d..b63a708 100644 --- a/services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts +++ b/services/request/src/server/services/WebServer/requests/MovieSearchRequest.ts @@ -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, { diff --git a/services/request/src/server/services/WebServer/requests/RegisterRequest.ts b/services/request/src/server/services/WebServer/requests/RegisterRequest.ts index 72a360d..088d4c3 100644 --- a/services/request/src/server/services/WebServer/requests/RegisterRequest.ts +++ b/services/request/src/server/services/WebServer/requests/RegisterRequest.ts @@ -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 diff --git a/services/request/src/server/services/WebServer/requests/Request.ts b/services/request/src/server/services/WebServer/requests/Request.ts deleted file mode 100644 index 8a5ef20..0000000 --- a/services/request/src/server/services/WebServer/requests/Request.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { FastifyRequest, FastifyReply } from "fastify"; -import { MiddlewareRequest } from "../middleware"; - -export default class Request -{ - /** - * Handle the incoming request - */ - public async handle(request: MiddlewareRequest, 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) { - return true; - } - - /** - * Check if the user is authorized to make this request - */ - public async isAuthorized(request: MiddlewareRequest) { - return true; - } - - /** - * Validate the request and return any errors - */ - public async validate(request: MiddlewareRequest): Promise { - return undefined; - } -} - -Request.apply(undefined); diff --git a/services/request/src/server/services/WebServer/requests/RequestImdbMovieRequest.ts b/services/request/src/server/services/WebServer/requests/RequestImdbMovieRequest.ts index a531f8a..2949ec4 100644 --- a/services/request/src/server/services/WebServer/requests/RequestImdbMovieRequest.ts +++ b/services/request/src/server/services/WebServer/requests/RequestImdbMovieRequest.ts @@ -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 extends RequestMovieRequest +export class RequestImdbMovieRequest extends RequestMovieRequest { public validate(request: MiddlewareRequest) { return validate.async(request.params, { diff --git a/services/request/src/server/services/WebServer/requests/RequestMovieRequest.ts b/services/request/src/server/services/WebServer/requests/RequestMovieRequest.ts index ecb6a98..df65304 100644 --- a/services/request/src/server/services/WebServer/requests/RequestMovieRequest.ts +++ b/services/request/src/server/services/WebServer/requests/RequestMovieRequest.ts @@ -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 extends Request +export class RequestMovieRequest extends Request { /** * Ensure the user is able to request movies diff --git a/services/request/src/server/services/WebServer/requests/RequestTmdbMovieRequest.ts b/services/request/src/server/services/WebServer/requests/RequestTmdbMovieRequest.ts index c37ab35..d54a5de 100644 --- a/services/request/src/server/services/WebServer/requests/RequestTmdbMovieRequest.ts +++ b/services/request/src/server/services/WebServer/requests/RequestTmdbMovieRequest.ts @@ -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 extends RequestMovieRequest +export class RequestTmdbMovieRequest extends RequestMovieRequest { public validate(request: FastifyRequest) { return validate.async(request.params, { diff --git a/services/request/src/server/services/WebServer/requests/index.ts b/services/request/src/server/services/WebServer/requests/index.ts index 8fcaaf5..12702f4 100644 --- a/services/request/src/server/services/WebServer/requests/index.ts +++ b/services/request/src/server/services/WebServer/requests/index.ts @@ -1,23 +1,7 @@ -import Request from "./Request"; -import { MiddlewareRequest } from "../middleware"; -import { FastifyReply } from "fastify"; - -type RequestConstructor = new () => Request; -export type RouteMiddlewareHandler = (request: MiddlewareRequest, reply: FastifyReply) => Promise | void; - -export default function handle(requests: RequestConstructor[], handle: RouteMiddlewareHandler): RouteMiddlewareHandler -{ - 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 (handle)(fastifyRequest, fastifyReply); - } -} +export * from "./LinkDiscordRequest"; +export * from "./LoginRequest"; +export * from "./MovieSearchRequest"; +export * from "./RegisterRequest"; +export * from "./RequestImdbMovieRequest"; +export * from "./RequestMovieRequest"; +export * from "./RequestTmdbMovieRequest"; diff --git a/services/request/src/server/services/WebServer/routes/RouteRegisterFactory.ts b/services/request/src/server/services/WebServer/routes/RouteRegisterFactory.ts deleted file mode 100644 index dffb2f2..0000000 --- a/services/request/src/server/services/WebServer/routes/RouteRegisterFactory.ts +++ /dev/null @@ -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 = never> = ((factory: RouteRegisterFactory) => void) - | ((factory: RouteRegisterFactory, app: Application) => void); - -export default class RouteRegisterFactory = MiddlewareMethod> -{ - /** - * 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>(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>(middleware: T[], factory: RouteFactory<(M|T)>) { - let middlewareBackup = this.middleware; - this.middleware = this.middleware.concat(middleware); - factory(>this, this.app); - this.middleware = middlewareBackup; - } - - /** - * Register a GET request - */ - public get>(path: string, handler: HandlerMethodWithMiddleware): void; - public get>(path: string, middleware: T[], handler: HandlerMethodWithMiddleware<(T|M)[]>): void; - public get>(path: string, middleware: HandlerMethodWithMiddleware<(T|M)[]>|T[], handler?: HandlerMethodWithMiddleware<(T|M)[]>) { - type Handler = HandlerMethodWithMiddleware<(T|M)[]>; - handler = (handler ?? middleware); - middleware = (middleware instanceof Array) ? this.middleware.concat(middleware) : this.middleware; - this.fastify.get(`${this.pathPrefix}${path}`, handleMiddleware(<(T|M)[]>middleware, handler)); - } - - /** - * Register a POST request - */ - public post>(path: string, handler: HandlerMethodWithMiddleware): void; - public post>(path: string, middleware: T[], handler: HandlerMethodWithMiddleware<(T|M)[]>): void; - public post>(path: string, middleware: HandlerMethodWithMiddleware<(T|M)[]>|T[], handler?: HandlerMethodWithMiddleware<(T|M)[]>) { - type Handler = HandlerMethodWithMiddleware<(T|M)[]>; - handler = (handler ?? middleware); - middleware = (middleware instanceof Array) ? this.middleware.concat(middleware) : this.middleware; - this.fastify.post(`${this.pathPrefix}${path}`, handleMiddleware(<(T|M)[]>middleware, handler)); - } - - /** - * Register a proxy route - */ - public proxy>(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(middleware)) : undefined, - upstream - }); - } - - /** - * Log under the WebServer service - */ - public log(...args: any[]) { - this.webserver.log(...args); - } -} diff --git a/services/request/src/server/services/WebServer/routes/api.ts b/services/request/src/server/services/WebServer/routes/api.ts index f0fd81d..0ad80c4 100644 --- a/services/request/src/server/services/WebServer/routes/api.ts +++ b/services/request/src/server/services/WebServer/routes/api.ts @@ -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: A, b: B) { + +} /** * Register API routes */ -export default function register(factory: RouteRegisterFactory, app: Application) { +export default function register(factory: RouteRegisterFactory, 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 = (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 = (request.params)["imdb_id"]; // let title = (request.query)["title"] || null; diff --git a/services/request/src/server/services/WebServer/routes/auth.ts b/services/request/src/server/services/WebServer/routes/auth.ts index 05e3d68..a34ddfe 100644 --- a/services/request/src/server/services/WebServer/routes/auth.ts +++ b/services/request/src/server/services/WebServer/routes/auth.ts @@ -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, 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 = 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 = 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 = (request.params)["token"]; let linkRequest = await DiscordLinkRequest.findOne({ where: { token } });