initial
[sketchy-heroes.git] / src / lib / server.ts
1 import express, { Express } from 'express';
2 import cors from 'cors';
3 import { v4 as uuid } from 'uuid';
4 import { logger } from './logger';
5 import * as time from './time';
6 import Ajv from 'ajv';
7 import {BadInputError, ForbiddenError} from './http-errors';
8 import addFormats from 'ajv-formats';
9 import {prisma} from './db';
10 import path from 'path';
11
12 export enum HTTP_METHOD {
13   GET = 'get',
14     POST = 'post',
15     DELETE = 'delete',
16     PATCH = 'patch'
17 };
18
19 export type ApiEndpointHandler<Input, Output> = (input: Input, server: ApiServer) => Promise<Output>;
20
21 export interface WrappedApiEndpointHandler {
22   method: HTTP_METHOD,
23   path: string,
24   handler: (req: express.Request, res: express.Response) => void;
25 }
26
27 const ajv = addFormats(new Ajv({}), [
28     'date-time', 
29     'time', 
30     'date', 
31     'email',  
32     'hostname', 
33     'ipv4', 
34     'ipv6', 
35     'uri', 
36     'uri-reference', 
37     'uuid',
38     'uri-template', 
39     'json-pointer', 
40     'relative-json-pointer', 
41     'regex'
42 ]).addKeyword('kind').addKeyword('modifier')
43
44 export interface EndpointOptions {
45   schema?: any;
46   authenicated?: boolean
47 }
48
49
50 export class ApiServer {
51   express: Express;
52   ajv: Ajv;
53
54   constructor() {
55     this.express = express();
56
57     this.express.use(express.json());
58     this.express.use(cors());
59     this.express.use(express.static(path.join(__dirname, '..', 'public')));
60
61     this.ajv = ajv;
62   }
63
64   APIEndpointWrapper<Input, Output>(type: HTTP_METHOD, path: string, handler: ApiEndpointHandler<Input, Output>, options: EndpointOptions = {}): WrappedApiEndpointHandler {
65     logger.info(`Register ${type} ${path}`);
66     const hasSchema = options.schema;
67     const self = this;
68     return {
69       method: type,
70       path: path,
71       handler: async (req: express.Request, res: express.Response) => {
72         const start = Date.now();
73         let meta = {
74           id: uuid(),
75           processingTime: 0,
76           gameTime: time.now()
77         };
78
79         logger.info(`Req: ${req.method} ${req.path}`);
80         try {
81           const params = {
82             params: req.params,
83             body: req.body
84           } as unknown;
85
86           if(hasSchema && !this.ajv.validate(options.schema, params)) {
87             if(this.ajv.errors) {
88               throw new BadInputError(this.ajv.errors.map(e => e.message).join('.'));
89             }
90             else {
91               throw new BadInputError();
92             }
93           }
94
95           if(options.authenicated) {
96             // lets use the token header and validate this request
97             const token = await prisma.authToken.findUnique({
98               where: {
99                 token: req.header('x-auth-token')
100               }
101             });
102
103             if(!token) {
104               throw new ForbiddenError('Invalid x-auth-token');
105             }
106           }
107           
108           const output: Output = await handler(params as Input, self);
109           meta.processingTime = Date.now() - start;
110
111           res.json({
112             status: 'ok',
113             payload: output,
114             meta
115           })
116         }
117         catch(e: any) {
118           logger.error(e);
119           meta.processingTime = Date.now() - start;
120           res.json({
121             status: 'error',
122             payload: e.message,
123             statusCode: e.statusCode ? e.statusCode : 500,
124             meta
125           });
126         }
127       }
128     };
129   }
130
131   get<I, O>(path: string, schema: EndpointOptions, handler: ApiEndpointHandler<I, O>) {
132     return this.APIEndpointWrapper<I, O>(HTTP_METHOD.GET, path, handler, schema);
133   }
134
135   post<I, O>(path: string, schema: EndpointOptions, handler: ApiEndpointHandler<I, O>) {
136     return this.APIEndpointWrapper<I, O>(HTTP_METHOD.POST, path, handler, schema);
137   }
138
139   patch<I, O>(path: string, schema: EndpointOptions, handler: ApiEndpointHandler<I, O>) {
140     return this.APIEndpointWrapper<I, O>(HTTP_METHOD.PATCH, path, handler, schema);
141   }
142
143   start(port: string) {
144     return new Promise(res => {
145       this.express.listen(port, () => {
146         logger.info(`Listening on port ${port}`);
147         res({port: port});
148       });
149     });
150   }
151 }
152
153 export const server = new ApiServer();