typescript(lunargame/api): basic jest setup
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
		
					parent
					
						
							
								175cea413f
							
						
					
				
			
			
				commit
				
					
						dfab3750f9
					
				
			
		
					 50 changed files with 3313 additions and 743 deletions
				
			
		typescript/lunargame/api
.gitignore
db/migrations
jest.config.jsknexfile.tsmigrations
package-lock.jsonpackage.jsonseeds
src
common/lang/arrays/helpers
modules
auth
authSchemas.tsconstants.ts
helpers
middleware
queries
createPassword.tscreateUser.tsemailIsTaken.tsgetAccountWithPassword.tsgetPublicAccountData.tsgetUserById.tsverifyEmail.ts
routes
types
core
db
game
network
							
								
								
									
										4
									
								
								typescript/lunargame/api/.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								typescript/lunargame/api/.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,2 +1,4 @@ | ||||||
| node_modules | node_modules | ||||||
| .env | .env | ||||||
|  | test/db.sqlite | ||||||
|  | db/db.sqlite | ||||||
							
								
								
									
										17
									
								
								typescript/lunargame/api/db/migrations/create_simulation.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								typescript/lunargame/api/db/migrations/create_simulation.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,17 @@ | ||||||
|  | // in case i want to change it
 | ||||||
|  | // it's alwys a pain to change it everywhere
 | ||||||
|  | const tableName = 'simulation' | ||||||
|  | 
 | ||||||
|  | exports.up = function up(knex) { | ||||||
|  |     return knex.schema.createTable(tableName, table => { | ||||||
|  |         // this is the id of the simulation
 | ||||||
|  |         table.increments() | ||||||
|  | 
 | ||||||
|  |         // this is the actual name of the simulation
 | ||||||
|  |         table.text('name').notNull() | ||||||
|  |     }) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | exports.down = function down(knex) { | ||||||
|  |     return knex.schema.dropTable(tableName) | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								typescript/lunargame/api/jest.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								typescript/lunargame/api/jest.config.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | module.exports = { | ||||||
|  |     roots: ['<rootDir>/src'], | ||||||
|  |     transform: { | ||||||
|  |         '^.+\\.tsx?$': 'ts-jest' | ||||||
|  |     }, | ||||||
|  |     testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$', | ||||||
|  |     moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], | ||||||
|  |     testEnvironment: 'node' | ||||||
|  | } | ||||||
|  | @ -1,17 +1,53 @@ | ||||||
| export const development = { | import { iNode_env } from './src/modules/core/node_env' | ||||||
|     client: 'pg', | import { Config } from 'knex' | ||||||
|     connection: { | import { resolve } from 'path' | ||||||
|         port: '5432', | 
 | ||||||
|         host: 'localhost', | // This is the name of the db file
 | ||||||
|         database: 'lunarbox', | const dbName = 'db.sqlite' | ||||||
|         user: 'postgres', | 
 | ||||||
|         password: 'drielrafael11' | // Ive made those to prevent repetition
 | ||||||
|  | const dbFolder = resolve(__dirname, 'db') | ||||||
|  | const testFolder = resolve(__dirname, 'test') | ||||||
|  | 
 | ||||||
|  | // This is used in all configs
 | ||||||
|  | const migrations: Config['migrations'] = { | ||||||
|  |     directory: resolve(dbFolder, 'migrations'), | ||||||
|  |     tableName: 'migrations' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // This is the confg we are going to esport
 | ||||||
|  | // Im making a separate variable instead of
 | ||||||
|  | // default exporting it because i want to
 | ||||||
|  | // also eport each prop by name
 | ||||||
|  | const config: Partial<Record<iNode_env, Config>> = { | ||||||
|  |     development: { | ||||||
|  |         client: 'sqlite3', | ||||||
|  |         connection: { | ||||||
|  |             filename: resolve(dbFolder, dbName) | ||||||
|  |         }, | ||||||
|  |         migrations, | ||||||
|  |         seeds: { | ||||||
|  |             directory: resolve(dbFolder, 'seeds') | ||||||
|  |         } | ||||||
|     }, |     }, | ||||||
|     migrations: { |     test: { | ||||||
|         directory: './migrations', |         client: 'sqlite3', | ||||||
|         tablename: 'migrations' |         connection: { | ||||||
|     }, |             filename: resolve(testFolder, dbName) | ||||||
|     seeds: { |         }, | ||||||
|         directory: './seeds' |         migrations, | ||||||
|  |         seeds: { | ||||||
|  |             directory: resolve(testFolder, 'seeds') | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | // These are exposed to knex
 | ||||||
|  | const { development, test } = config | ||||||
|  | 
 | ||||||
|  | // This is the export wich should be used in th eactua app
 | ||||||
|  | export default config | ||||||
|  | 
 | ||||||
|  | // For migartions to work
 | ||||||
|  | // If i dont include this knex will throw an error
 | ||||||
|  | export { development, test } | ||||||
|  |  | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| exports.up = function up(knex) { |  | ||||||
|     return knex.schema.createTable('account', table => { |  | ||||||
|         table.text('uid').primary() |  | ||||||
|         table.text('verificationToken').notNullable() |  | ||||||
|         table.text('name').notNullable() |  | ||||||
|         table.text('email').notNullable() |  | ||||||
|         table |  | ||||||
|             .text('description') |  | ||||||
|             .defaultTo('') |  | ||||||
|             .notNullable() |  | ||||||
|         table |  | ||||||
|             .text('avatar') |  | ||||||
|             .defaultTo('') |  | ||||||
|             .notNullable() |  | ||||||
|         table |  | ||||||
|             .boolean('verified') |  | ||||||
|             .defaultTo(false) |  | ||||||
|             .notNullable() |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| exports.down = function down(knex) { |  | ||||||
|     return knex.schema.dropTable('account') |  | ||||||
| } |  | ||||||
|  | @ -1,29 +0,0 @@ | ||||||
| exports.up = function up(knex) { |  | ||||||
|     return knex.schema.createTable('game', table => { |  | ||||||
|         table.increments() |  | ||||||
|         table |  | ||||||
|             .text('name') |  | ||||||
|             .notNull() |  | ||||||
|             .defaultTo('myGame') |  | ||||||
|         table |  | ||||||
|             .text('avatar') |  | ||||||
|             .notNull() |  | ||||||
|             .defaultTo('') |  | ||||||
|         table |  | ||||||
|             .text('thumbail') |  | ||||||
|             .notNull() |  | ||||||
|             .defaultTo('') |  | ||||||
|         table |  | ||||||
|             .text('description') |  | ||||||
|             .notNull() |  | ||||||
|             .defaultTo('This is my game!') |  | ||||||
|         table |  | ||||||
|             .boolean('public') |  | ||||||
|             .notNull() |  | ||||||
|             .defaultTo(false) |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| exports.down = function down(knex) { |  | ||||||
|     return knex.schema.dropTable('game') |  | ||||||
| } |  | ||||||
|  | @ -1,15 +0,0 @@ | ||||||
| exports.up = function up(knex) { |  | ||||||
|     return knex.schema.createTable('user-password', table => { |  | ||||||
|         table.increments() |  | ||||||
|         table.text('uid').notNull() |  | ||||||
|         table.text('value').notNull() |  | ||||||
|         table |  | ||||||
|             .boolean('secure') |  | ||||||
|             .defaultTo(true) |  | ||||||
|             .notNull() |  | ||||||
|     }) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| exports.down = function down(knex) { |  | ||||||
|     return knex.schema.dropTable('user-password') |  | ||||||
| } |  | ||||||
							
								
								
									
										3151
									
								
								typescript/lunargame/api/package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										3151
									
								
								typescript/lunargame/api/package-lock.json
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -3,13 +3,15 @@ | ||||||
|     "version": "1.0.0", |     "version": "1.0.0", | ||||||
|     "scripts": { |     "scripts": { | ||||||
|         "start": "nodemon", |         "start": "nodemon", | ||||||
|         "reset:db": "knex migrate:rollback && knex migrate:latest && knex seed:run" |         "reset:db": "knex migrate:rollback && knex migrate:latest && knex seed:run", | ||||||
|  |         "test": "cross-env NODE_ENV=test && jest --runInBand" | ||||||
|     }, |     }, | ||||||
|     "main": "index.js", |     "main": "index.js", | ||||||
|     "private": true, |     "private": true, | ||||||
|     "devDependencies": { |     "devDependencies": { | ||||||
|         "@types/bcryptjs": "^2.4.2", |         "@types/bcryptjs": "^2.4.2", | ||||||
|         "@types/dotenv": "^6.1.1", |         "@types/dotenv": "^6.1.1", | ||||||
|  |         "@types/jest": "^24.0.17", | ||||||
|         "@types/joi": "^14.3.3", |         "@types/joi": "^14.3.3", | ||||||
|         "@types/koa": "^2.0.49", |         "@types/koa": "^2.0.49", | ||||||
|         "@types/koa-bodyparser": "^4.3.0", |         "@types/koa-bodyparser": "^4.3.0", | ||||||
|  | @ -19,7 +21,11 @@ | ||||||
|         "@types/node": "^12.0.10", |         "@types/node": "^12.0.10", | ||||||
|         "@types/nodemailer": "^6.2.0", |         "@types/nodemailer": "^6.2.0", | ||||||
|         "@types/uuid": "^3.4.5", |         "@types/uuid": "^3.4.5", | ||||||
|  |         "cross-env": "^5.2.0", | ||||||
|  |         "jest": "^24.8.0", | ||||||
|         "nodemon": "^1.19.1", |         "nodemon": "^1.19.1", | ||||||
|  |         "sqlite3": "^4.0.9", | ||||||
|  |         "ts-jest": "^24.0.2", | ||||||
|         "ts-node": "^8.3.0", |         "ts-node": "^8.3.0", | ||||||
|         "typescript": "^3.5.2" |         "typescript": "^3.5.2" | ||||||
|     }, |     }, | ||||||
|  | @ -32,7 +38,6 @@ | ||||||
|         "joi": "^14.3.1", |         "joi": "^14.3.1", | ||||||
|         "knex": "^0.18.1", |         "knex": "^0.18.1", | ||||||
|         "koa": "^2.7.0", |         "koa": "^2.7.0", | ||||||
|         "koa-async-validator": "^0.4.1", |  | ||||||
|         "koa-bodyparser": "^4.2.1", |         "koa-bodyparser": "^4.2.1", | ||||||
|         "koa-router": "^7.4.0", |         "koa-router": "^7.4.0", | ||||||
|         "koa-session": "^5.12.0", |         "koa-session": "^5.12.0", | ||||||
|  |  | ||||||
|  | @ -1,19 +0,0 @@ | ||||||
| import * as Knex from 'knex' |  | ||||||
| 
 |  | ||||||
| export async function seed(knex: Knex): Promise<any> { |  | ||||||
|     return knex('account') |  | ||||||
|         .del() |  | ||||||
|         .then(() => { |  | ||||||
|             return knex('account').insert([ |  | ||||||
|                 { |  | ||||||
|                     uid: '1', |  | ||||||
|                     email: 'rafaeladriel11@gmail.com', |  | ||||||
|                     name: 'Mock account', |  | ||||||
|                     description: 'just a mock account', |  | ||||||
|                     verificationToken: '0123456789', |  | ||||||
|                     avatar: |  | ||||||
|                         'https://cdn.vox-cdn.com/thumbor/YuWeAOQKc880Dpo1NYGS1sDBG4A=/1400x1400/filters:format(png)/cdn.vox-cdn.com/uploads/chorus_asset/file/13591799/Screen_Shot_2018_11_30_at_9.47.55_AM.png' |  | ||||||
|                 } |  | ||||||
|             ]) |  | ||||||
|         }) |  | ||||||
| } |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| import * as Knex from 'knex' |  | ||||||
| 
 |  | ||||||
| export async function seed(knex: Knex): Promise<any> { |  | ||||||
|     return knex('user-password') |  | ||||||
|         .del() |  | ||||||
|         .then(() => { |  | ||||||
|             return knex('user-password').insert([ |  | ||||||
|                 { uid: 1, value: '7777', secure: false } |  | ||||||
|             ]) |  | ||||||
|         }) |  | ||||||
| } |  | ||||||
|  | @ -1,51 +0,0 @@ | ||||||
| import * as Knex from 'knex' |  | ||||||
| 
 |  | ||||||
| export async function seed(knex: Knex): Promise<any> { |  | ||||||
|     return knex('game') |  | ||||||
|         .del() |  | ||||||
|         .then(() => { |  | ||||||
|             return knex('game').insert( |  | ||||||
|                 [...Array(300)] |  | ||||||
|                     .fill(true) |  | ||||||
|                     .map(() => [ |  | ||||||
|                         { |  | ||||||
|                             name: 'Spacefilght Simularor', |  | ||||||
|                             description: |  | ||||||
|                                 'A simulator where you build & fly rockets', |  | ||||||
|                             public: true, |  | ||||||
|                             thumbail: |  | ||||||
|                                 'https://is3-ssl.mzstatic.com/image/thumb/Purple118/v4/fa/72/0f/fa720ff4-accb-85de-e558-71b1821399c8/source/512x512bb.jpg' |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             name: 'Rocket league', |  | ||||||
|                             description: 'Basically football - but for cars', |  | ||||||
|                             public: true, |  | ||||||
|                             thumbail: |  | ||||||
|                                 'https://steamcdn-a.akamaihd.net/steam/apps/252950/ss_b7e945ac18d86c48b279f26ff6884b5ded2aa1b7.1920x1080.jpg?t=1561064854' |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             name: 'Portal', |  | ||||||
|                             description: 'The cake is a lie', |  | ||||||
|                             public: true, |  | ||||||
|                             thumbail: |  | ||||||
|                                 'https://upload.wikimedia.org/wikipedia/en/thumb/9/9f/Portal_standalonebox.jpg/220px-Portal_standalonebox.jpg' |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             name: 'Portal 2', |  | ||||||
|                             description: 'The cake is still a lie', |  | ||||||
|                             public: true, |  | ||||||
|                             thumbail: |  | ||||||
|                                 'https://steamcdn-a.akamaihd.net/steam/apps/620/ss_8a772608d29ffd56ac013d2ac7c4388b96e87a21.1920x1080.jpg?t=1512411524' |  | ||||||
|                         }, |  | ||||||
|                         { |  | ||||||
|                             name: 'The Stanley parable', |  | ||||||
|                             description: 'And Stanley... was happy', |  | ||||||
|                             public: true, |  | ||||||
|                             thumbail: |  | ||||||
|                                 'https://steamcdn-a.akamaihd.net/steam/apps/221910/ss_49e682563292992309e3047f30128f3dba4c39ce.1920x1080.jpg?t=1465254276' |  | ||||||
|                         } |  | ||||||
|                     ]) |  | ||||||
|                     .flat() |  | ||||||
|             ) |  | ||||||
|         }) |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | import { randomElement } from './randomElement' | ||||||
|  | 
 | ||||||
|  | describe('The randomElement function', () => { | ||||||
|  |     test('should return the only element in an array of length 1', () => { | ||||||
|  |         const element = 7 | ||||||
|  | 
 | ||||||
|  |         expect(randomElement([element])).toBe(element) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('should throw an error when passing an empty array', () => { | ||||||
|  |         let error: Error | undefined | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             randomElement([]) | ||||||
|  |         } catch (catchedError) { | ||||||
|  |             //
 | ||||||
|  |             error = catchedError | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         expect(error).toBeTruthy() | ||||||
|  |     }) | ||||||
|  | }) | ||||||
|  | @ -0,0 +1,13 @@ | ||||||
|  | /** | ||||||
|  |  * Returns a random element from an array | ||||||
|  |  * | ||||||
|  |  * @param arr The array to select the element from | ||||||
|  |  * @throws Error if the array has length 0 | ||||||
|  |  */ | ||||||
|  | export const randomElement = <T>(arr: T[]): T => { | ||||||
|  |     if (!arr.length) { | ||||||
|  |         throw new Error('Cannot choose a random element from array of length 0') | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return arr[Math.floor(arr.length * Math.random())] | ||||||
|  | } | ||||||
|  | @ -1,29 +0,0 @@ | ||||||
| import Joi from 'joi' |  | ||||||
| 
 |  | ||||||
| export const token = Joi.string().required() |  | ||||||
| export const field = token.alphanum() |  | ||||||
| 
 |  | ||||||
| // not merging them cause i'll add more to the password in the future
 |  | ||||||
| export const name = field.max(30).min(3) |  | ||||||
| export const password = field.min(3).max(30) |  | ||||||
| 
 |  | ||||||
| export const email = Joi.string() |  | ||||||
|     .required() |  | ||||||
|     .email() |  | ||||||
|     .min(3) |  | ||||||
|     .max(30) |  | ||||||
| 
 |  | ||||||
| export const hasName = Joi.object({ name }) |  | ||||||
| export const hasEmail = Joi.object({ email }) |  | ||||||
| export const hasVerificationToken = Joi.object({ token }) |  | ||||||
| 
 |  | ||||||
| export const loginSchema = Joi.object({ |  | ||||||
|     email, |  | ||||||
|     password |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| export const createUserSchema = Joi.object({ |  | ||||||
|     name, |  | ||||||
|     email, |  | ||||||
|     password |  | ||||||
| }) |  | ||||||
|  | @ -1,15 +0,0 @@ | ||||||
| import { Account } from './types/Account' |  | ||||||
| 
 |  | ||||||
| export type AccountField = keyof Account |  | ||||||
| 
 |  | ||||||
| export const publicAccountFields: AccountField[] = [ |  | ||||||
|     'name', |  | ||||||
|     'email', |  | ||||||
|     'description', |  | ||||||
|     'avatar' |  | ||||||
| ] |  | ||||||
| export const privateAccountFields: AccountField[] = [ |  | ||||||
|     ...publicAccountFields, |  | ||||||
|     'verified', |  | ||||||
|     'uid' |  | ||||||
| ] |  | ||||||
|  | @ -1,17 +0,0 @@ | ||||||
| import { privateAccountFields } from '../constants' |  | ||||||
| import { Account } from '../types/Account' |  | ||||||
| 
 |  | ||||||
| export const filterPrivateAccountData = <T>(account: Account & T) => { |  | ||||||
|     const result: Record<string, unknown> = {} |  | ||||||
| 
 |  | ||||||
|     for (const key of Object.keys(account)) { |  | ||||||
|         // for ts to shut up
 |  | ||||||
|         const typedKey = (key as unknown) as keyof Account |  | ||||||
| 
 |  | ||||||
|         if (privateAccountFields.includes(typedKey)) { |  | ||||||
|             result[typedKey] = account[typedKey] |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return result |  | ||||||
| } |  | ||||||
|  | @ -1,17 +0,0 @@ | ||||||
| import { Account } from '../types/Account' |  | ||||||
| import { Password } from '../types/Password' |  | ||||||
| import { compare } from 'bcryptjs' |  | ||||||
| 
 |  | ||||||
| export const checkPassword = async ( |  | ||||||
|     account: Account & Password, |  | ||||||
|     password: string |  | ||||||
| ) => { |  | ||||||
|     if ( |  | ||||||
|         (account.secure && (await compare(password, account.value))) || // prod
 |  | ||||||
|         (!account.secure && account.value === password) // dev
 |  | ||||||
|     ) { |  | ||||||
|         return true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return false |  | ||||||
| } |  | ||||||
|  | @ -1,8 +0,0 @@ | ||||||
| import { genSalt, hash } from 'bcryptjs' |  | ||||||
| 
 |  | ||||||
| export const encryptPassword = async (saltRounds = 10, password: string) => { |  | ||||||
|     const salt = await genSalt(saltRounds) |  | ||||||
|     const passwordHash = await hash(password, salt) |  | ||||||
| 
 |  | ||||||
|     return passwordHash |  | ||||||
| } |  | ||||||
|  | @ -1,6 +0,0 @@ | ||||||
| export const subject = 'Lunarbox verification' |  | ||||||
| export const text = (token: string, name: string) => ` |  | ||||||
|     Hey ${name}! Welcome to lunarbox! To verify your email, click the link bellow: ${ |  | ||||||
|     process.env.SERVER_URL |  | ||||||
| }/account/verify/${token} |  | ||||||
| ` |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| import { Middleware } from 'koa' |  | ||||||
| import { HttpError } from '../../network/classes/HttpError' |  | ||||||
| 
 |  | ||||||
| export const isGuest = (): Middleware => (context, next) => { |  | ||||||
|     if (context.session.uid === null || context.session.uid === undefined) |  | ||||||
|         return next() |  | ||||||
| 
 |  | ||||||
|     throw new HttpError(400, 'Logged in.') |  | ||||||
| } |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| import { Middleware } from 'koa' |  | ||||||
| import { HttpError } from '../../network/classes/HttpError' |  | ||||||
| 
 |  | ||||||
| export const notGuest = (): Middleware => (context, next) => { |  | ||||||
|     if (context.session.uid === null || context.session.uid === undefined) |  | ||||||
|         throw new HttpError(401) |  | ||||||
| 
 |  | ||||||
|     return next() |  | ||||||
| } |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| import { Middleware } from 'koa' |  | ||||||
| import { emailIsTaken } from '../queries/emailIsTaken' |  | ||||||
| import { HttpError } from '../../network/classes/HttpError' |  | ||||||
| 
 |  | ||||||
| export const uniqueEmail = (): Middleware => async (context, next) => { |  | ||||||
|     const { email } = context.request.body |  | ||||||
| 
 |  | ||||||
|     if (!(await emailIsTaken(email))) { |  | ||||||
|         return next() |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     throw new HttpError(400, 'Email is already in use.') |  | ||||||
| } |  | ||||||
|  | @ -1,25 +0,0 @@ | ||||||
| import { Middleware } from 'koa' |  | ||||||
| import { HttpError } from '../../network/classes/HttpError' |  | ||||||
| import { mode } from '../../core/constants' |  | ||||||
| 
 |  | ||||||
| const strongRegex = new RegExp( |  | ||||||
|     '^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})' |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| export const validatePassword = (prouctondOnly = true): Middleware => ( |  | ||||||
|     context, |  | ||||||
|     next |  | ||||||
| ) => { |  | ||||||
|     const password = context.request.body.password |  | ||||||
| 
 |  | ||||||
|     if (!password) { |  | ||||||
|         throw new HttpError(400, 'No password recived.') |  | ||||||
|     } else if ( |  | ||||||
|         (mode === 'production' || !prouctondOnly) && |  | ||||||
|         !strongRegex.test(context.request.body.password) |  | ||||||
|     ) { |  | ||||||
|         throw new HttpError(400, 'Bad password.') |  | ||||||
|     } else { |  | ||||||
|         return next() |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -1,18 +0,0 @@ | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| import { Password } from '../types/Password' |  | ||||||
| 
 |  | ||||||
| const { connection } = new DbManager() |  | ||||||
| 
 |  | ||||||
| export const createPassword = ( |  | ||||||
|     password: string, |  | ||||||
|     uid: string |  | ||||||
| ): Promise<Password> => { |  | ||||||
|     return connection |  | ||||||
|         .from('user-password') |  | ||||||
|         .insert<Password>({ |  | ||||||
|             secure: true, |  | ||||||
|             uid, |  | ||||||
|             value: password |  | ||||||
|         }) |  | ||||||
|         .returning('*') |  | ||||||
| } |  | ||||||
|  | @ -1,29 +0,0 @@ | ||||||
| import { Account } from '../types/Account' |  | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| 
 |  | ||||||
| const { connection } = new DbManager() |  | ||||||
| 
 |  | ||||||
| const defaultAvatar = |  | ||||||
|     'https://themango.co/wp-content/uploads/2018/03/Mango-Default-Profile-Pic.png' |  | ||||||
| 
 |  | ||||||
| export const createUser = async ( |  | ||||||
|     name: string, |  | ||||||
|     email: string, |  | ||||||
|     uid: string, |  | ||||||
|     token: string |  | ||||||
| ) => { |  | ||||||
|     const data: Account = { |  | ||||||
|         name, |  | ||||||
|         email, |  | ||||||
|         uid, |  | ||||||
|         avatar: defaultAvatar, |  | ||||||
|         description: '', |  | ||||||
|         verified: false, |  | ||||||
|         verificationToken: token |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return connection |  | ||||||
|         .from('account') |  | ||||||
|         .insert(data) |  | ||||||
|         .returning('*') |  | ||||||
| } |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| 
 |  | ||||||
| const { connection } = new DbManager() |  | ||||||
| 
 |  | ||||||
| export const emailIsTaken = async (email: string) => { |  | ||||||
|     return await connection |  | ||||||
|         .from('account') |  | ||||||
|         .select('email') |  | ||||||
|         .where('email', email) |  | ||||||
|         .first() |  | ||||||
| } |  | ||||||
|  | @ -1,17 +0,0 @@ | ||||||
| import { Account } from '../types/Account' |  | ||||||
| import { Password } from '../types/Password' |  | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| import { publicAccountFields, AccountField } from '../constants' |  | ||||||
| 
 |  | ||||||
| const db = new DbManager() |  | ||||||
| 
 |  | ||||||
| export function getUserWithPassword( |  | ||||||
|     field: string, |  | ||||||
|     value: string |  | ||||||
| ): Promise<Account & Password | null> { |  | ||||||
|     return db.connection |  | ||||||
|         .from('account') |  | ||||||
|         .innerJoin('user-password', 'account.uid', 'user-password.uid') |  | ||||||
|         .where(`account.${field}`, value) |  | ||||||
|         .first() |  | ||||||
| } |  | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| import { publicAccountFields } from '../constants' |  | ||||||
| 
 |  | ||||||
| const { connection } = new DbManager() |  | ||||||
| 
 |  | ||||||
| export const getPublicAccountData = (field: string, value: string) => { |  | ||||||
|     return connection |  | ||||||
|         .from('account') |  | ||||||
|         .select(...publicAccountFields) |  | ||||||
|         .where(field, value) |  | ||||||
|         .first() |  | ||||||
| } |  | ||||||
|  | @ -1,12 +0,0 @@ | ||||||
| import { Account } from '../types/Account' |  | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| 
 |  | ||||||
| const db = new DbManager() |  | ||||||
| 
 |  | ||||||
| export function getUserByUid(uid: string): Promise<Account | null> { |  | ||||||
|     return db.connection |  | ||||||
|         .from('account') |  | ||||||
|         .select('*') |  | ||||||
|         .where('uid', uid) |  | ||||||
|         .first() |  | ||||||
| } |  | ||||||
|  | @ -1,11 +0,0 @@ | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| 
 |  | ||||||
| const { connection } = new DbManager() |  | ||||||
| 
 |  | ||||||
| export const verifyAccount = (token: string) => { |  | ||||||
|     return connection |  | ||||||
|         .from('account') |  | ||||||
|         .update('verified', true) |  | ||||||
|         .where('verificationToken', token) |  | ||||||
|         .returning('verified') |  | ||||||
| } |  | ||||||
|  | @ -1,59 +0,0 @@ | ||||||
| import { getUserByUid } from '../queries/getUserById' |  | ||||||
| import Router from 'koa-router' |  | ||||||
| import { HttpError } from '../../network/classes/HttpError' |  | ||||||
| import { notGuest } from '../middleware/notGuest' |  | ||||||
| import { verifyAccount } from '../queries/verifyEmail' |  | ||||||
| import { mode } from '../../core/constants' |  | ||||||
| import { filterPrivateAccountData } from '../helpers/accountDataFilters' |  | ||||||
| import { validate } from '../../../common/validation/middleware/validate' |  | ||||||
| import { hasVerificationToken } from '../authSchemas' |  | ||||||
| 
 |  | ||||||
| const router = new Router() |  | ||||||
| 
 |  | ||||||
| router.get('/uid', notGuest(), async (context, next) => { |  | ||||||
|     const uid: string = context.session.uid |  | ||||||
| 
 |  | ||||||
|     context.body = { |  | ||||||
|         uid |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     next() |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| router.delete('/uid', (context, next) => { |  | ||||||
|     context.session.uid = undefined |  | ||||||
| 
 |  | ||||||
|     context.body = { |  | ||||||
|         succes: true |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     next() |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| router.get('/', notGuest(), async (context, next) => { |  | ||||||
|     const uid: string = context.session.uid |  | ||||||
|     const account = await getUserByUid(uid) |  | ||||||
| 
 |  | ||||||
|     if (!account) throw new HttpError(404) |  | ||||||
| 
 |  | ||||||
|     context.body = { |  | ||||||
|         data: filterPrivateAccountData(account) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     next() |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| router.get( |  | ||||||
|     '/verify/:token', |  | ||||||
|     validate(hasVerificationToken, 'params'), |  | ||||||
|     async (context, next) => { |  | ||||||
|         const token = context.params.token |  | ||||||
|         await verifyAccount(token) |  | ||||||
| 
 |  | ||||||
|         context.body = `Succesfully verified account!` |  | ||||||
| 
 |  | ||||||
|         return next() |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| export { router } |  | ||||||
|  | @ -1,75 +0,0 @@ | ||||||
| import uuid from 'uuid/v4' |  | ||||||
| import Router from 'koa-router' |  | ||||||
| import { getUserWithPassword } from '../queries/getAccountWithPassword' |  | ||||||
| import { checkPassword } from '../helpers/checkPassword' |  | ||||||
| import { HttpError } from '../../network/classes/HttpError' |  | ||||||
| import { isGuest } from '../middleware/isGuest' |  | ||||||
| import { createPassword } from '../queries/createPassword' |  | ||||||
| import { createUser } from '../queries/createUser' |  | ||||||
| import { encryptPassword } from '../helpers/encryptPassword' |  | ||||||
| import { uniqueEmail } from '../middleware/uniqueEmail' |  | ||||||
| import { EmailManager } from '../../network/classes/EmailManager' |  | ||||||
| import { subject, text } from '../helpers/verificationEmail' |  | ||||||
| import { validate } from '../../../common/validation/middleware/validate' |  | ||||||
| import { createUserSchema, hasEmail, loginSchema } from '../authSchemas' |  | ||||||
| import { filterPrivateAccountData } from '../helpers/accountDataFilters' |  | ||||||
| 
 |  | ||||||
| const router = new Router() |  | ||||||
| const emailManager = new EmailManager() |  | ||||||
| 
 |  | ||||||
| router.post( |  | ||||||
|     '/login', |  | ||||||
|     isGuest(), |  | ||||||
|     validate(loginSchema, 'body'), |  | ||||||
|     async (context, next) => { |  | ||||||
|         const account = await getUserWithPassword( |  | ||||||
|             'email', |  | ||||||
|             context.request.body.email |  | ||||||
|         ) |  | ||||||
| 
 |  | ||||||
|         const password: string = context.request.body.password |  | ||||||
| 
 |  | ||||||
|         if (!account) { |  | ||||||
|             throw new HttpError(400, "Account does't exist") |  | ||||||
|         } else if (!(await checkPassword(account, password))) { |  | ||||||
|             throw new HttpError(400, 'Wrong password') |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         context.session.uid = account.uid |  | ||||||
|         context.body = { |  | ||||||
|             data: filterPrivateAccountData(account) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return next() |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| router.post( |  | ||||||
|     '/create', |  | ||||||
|     isGuest(), |  | ||||||
|     validate(createUserSchema, 'body'), |  | ||||||
|     uniqueEmail(), |  | ||||||
|     async (context, next) => { |  | ||||||
|         const { email, name, password } = context.request.body |  | ||||||
| 
 |  | ||||||
|         const hash = await encryptPassword(10, password) |  | ||||||
|         const uid = uuid() |  | ||||||
|         const token = uuid() |  | ||||||
| 
 |  | ||||||
|         const [, account] = await Promise.all([ |  | ||||||
|             createPassword(hash, uid), |  | ||||||
|             createUser(name, email, uid, token), |  | ||||||
|             emailManager.send(email, subject, text(token, name)) |  | ||||||
|         ]) |  | ||||||
| 
 |  | ||||||
|         context.session.uid = uid |  | ||||||
| 
 |  | ||||||
|         context.body = { |  | ||||||
|             data: filterPrivateAccountData(account[0]) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return next() |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| export { router } |  | ||||||
|  | @ -1,26 +0,0 @@ | ||||||
| import Router from 'koa-router' |  | ||||||
| import { hasName } from '../authSchemas' |  | ||||||
| import { validate } from '../../../common/validation/middleware/validate' |  | ||||||
| import { getPublicAccountData } from '../queries/getPublicAccountData' |  | ||||||
| import { HttpError } from '../../network/classes/HttpError' |  | ||||||
| 
 |  | ||||||
| const router = new Router() |  | ||||||
| 
 |  | ||||||
| router.get( |  | ||||||
|     '/name/:name', |  | ||||||
|     validate(hasName, 'params'), |  | ||||||
|     async (context, next) => { |  | ||||||
|         const name: string = context.params.name |  | ||||||
|         const account = await getPublicAccountData('name', name) |  | ||||||
| 
 |  | ||||||
|         if (!account) throw new HttpError(404, `User ${name} does not exist.`) |  | ||||||
| 
 |  | ||||||
|         context.body = { |  | ||||||
|             data: account |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         return next() |  | ||||||
|     } |  | ||||||
| ) |  | ||||||
| 
 |  | ||||||
| export { router } |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| export interface Account { |  | ||||||
|     name: string |  | ||||||
|     email: string |  | ||||||
|     avatar: string |  | ||||||
|     description: string |  | ||||||
|     uid: string |  | ||||||
|     verified: boolean |  | ||||||
|     verificationToken: string |  | ||||||
| } |  | ||||||
|  | @ -1,5 +0,0 @@ | ||||||
| export interface Password { |  | ||||||
|     uid: string |  | ||||||
|     value: string |  | ||||||
|     secure: boolean |  | ||||||
| } |  | ||||||
|  | @ -1,4 +0,0 @@ | ||||||
| type processMode = 'development' | 'production' | 'test' |  | ||||||
| 
 |  | ||||||
| export const mode: processMode = |  | ||||||
|     (process.env.NODE_ENV as processMode) || 'development' |  | ||||||
							
								
								
									
										8
									
								
								typescript/lunargame/api/src/modules/core/node_env.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								typescript/lunargame/api/src/modules/core/node_env.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | ||||||
|  | // this is the type wich the node_env constant can take
 | ||||||
|  | export type iNode_env = 'development' | 'production' | 'test' | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Type safe version of process.env.NODE_ENV | ||||||
|  |  */ | ||||||
|  | export const node_env: iNode_env = | ||||||
|  |     (process.env.NODE_ENV as iNode_env) || 'development' | ||||||
|  | @ -1,14 +1,7 @@ | ||||||
| import { router as accountRouter } from '../auth/routes/account' |  | ||||||
| import { router as authRouter } from '../auth/routes/auth' |  | ||||||
| import { router as userRouter } from '../auth/routes/user' |  | ||||||
| import { router as gameRouter } from '../game/routes/game' |  | ||||||
| import Router from 'koa-router' | import Router from 'koa-router' | ||||||
| 
 | 
 | ||||||
| const router = new Router() | const router = new Router() | ||||||
| 
 | 
 | ||||||
| router.use('/account', accountRouter.middleware()) | router.use('/game') | ||||||
| router.use('/auth', authRouter.middleware()) |  | ||||||
| router.use('/user', userRouter.middleware()) |  | ||||||
| router.use('/game', gameRouter.middleware()) |  | ||||||
| 
 | 
 | ||||||
| export { router } | export { router } | ||||||
|  |  | ||||||
|  | @ -1,9 +0,0 @@ | ||||||
| import { Singleton } from '@eix/utils' |  | ||||||
| import * as config from '../../../../knexfile' |  | ||||||
| import knex from 'knex' |  | ||||||
| 
 |  | ||||||
| @Singleton |  | ||||||
| export class DbManager { |  | ||||||
|     public mode: 'development' = 'development' |  | ||||||
|     public connection = knex(config[this.mode]) |  | ||||||
| } |  | ||||||
							
								
								
									
										6
									
								
								typescript/lunargame/api/src/modules/db/connection.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								typescript/lunargame/api/src/modules/db/connection.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | import config from '../../../knexfile' | ||||||
|  | import knex, { Config } from 'knex' | ||||||
|  | import { node_env } from '../core/node_env' | ||||||
|  | 
 | ||||||
|  | // TODO: remove the as Config after finshnig the knexfile
 | ||||||
|  | export const connection = knex(config[node_env] as Config) | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| import Joi from 'joi' |  | ||||||
| 
 |  | ||||||
| export const pageSize = Joi.number() |  | ||||||
|     .required() |  | ||||||
|     .max(50) |  | ||||||
|     .min(3) |  | ||||||
| 
 |  | ||||||
| export const page = Joi.number().required() |  | ||||||
| 
 |  | ||||||
| export const chunkSchema = Joi.object({ |  | ||||||
|     pageSize, |  | ||||||
|     page |  | ||||||
| }) |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| 
 |  | ||||||
| const { connection } = new DbManager() |  | ||||||
| 
 |  | ||||||
| export const getGameChunk = (page: number, pageSize: number) => { |  | ||||||
|     const offset = page * pageSize |  | ||||||
| 
 |  | ||||||
|     return connection |  | ||||||
|         .from('game') |  | ||||||
|         .select('id', 'avatar', 'thumbail') |  | ||||||
|         .offset(offset) |  | ||||||
|         .limit(pageSize) |  | ||||||
| } |  | ||||||
|  | @ -1,13 +0,0 @@ | ||||||
| import { DbManager } from '../../db/classes/DbManager' |  | ||||||
| import { CountData } from '../../../common/rest/types/CountData' |  | ||||||
| 
 |  | ||||||
| const { connection } = new DbManager() |  | ||||||
| 
 |  | ||||||
| export const getGameCount = async () => { |  | ||||||
|     const { count } = await connection |  | ||||||
|         .from('game') |  | ||||||
|         .count('id') |  | ||||||
|         .first<CountData>() |  | ||||||
| 
 |  | ||||||
|     return Number(count) |  | ||||||
| } |  | ||||||
|  | @ -1,29 +0,0 @@ | ||||||
| import Router from 'koa-router' |  | ||||||
| import { validate } from '../../../common/validation/middleware/validate' |  | ||||||
| import { getGameCount } from '../queries/getGameCount' |  | ||||||
| import { chunkSchema } from '../gameSchemas' |  | ||||||
| import { getGameChunk } from '../queries/getGameChunk' |  | ||||||
| 
 |  | ||||||
| const router = new Router() |  | ||||||
| 
 |  | ||||||
| router.get('/count', async (context, next) => { |  | ||||||
|     const result = await getGameCount() |  | ||||||
| 
 |  | ||||||
|     context.body = { |  | ||||||
|         data: result |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return next() |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| router.get('/chunk', validate(chunkSchema, 'query'), async (context, next) => { |  | ||||||
|     const { page, pageSize } = context.request.query |  | ||||||
| 
 |  | ||||||
|     context.body = { |  | ||||||
|         data: await getGameChunk(page, pageSize) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return next() |  | ||||||
| }) |  | ||||||
| 
 |  | ||||||
| export { router } |  | ||||||
|  | @ -1,24 +0,0 @@ | ||||||
| import { Singleton } from '@eix/utils' |  | ||||||
| import { config } from 'dotenv' |  | ||||||
| import sendgrid from '@sendgrid/mail' |  | ||||||
| 
 |  | ||||||
| config() |  | ||||||
| 
 |  | ||||||
| @Singleton |  | ||||||
| export class EmailManager { |  | ||||||
|     private email_adress = process.env.EMAIL_ADRESS |  | ||||||
|     private key = process.env.SENDGRID_API_KEY |  | ||||||
| 
 |  | ||||||
|     constructor() { |  | ||||||
|         sendgrid.setApiKey(this.key) |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public send(to: string, subject: string, text: string) { |  | ||||||
|         return sendgrid.send({ |  | ||||||
|             from: this.email_adress, |  | ||||||
|             to, |  | ||||||
|             subject, |  | ||||||
|             text |  | ||||||
|         }) |  | ||||||
|     } |  | ||||||
| } |  | ||||||
|  | @ -0,0 +1,29 @@ | ||||||
|  | import { HttpError, HTTP_REASONS, HttpStatus, httpSymbol } from './HttpError' | ||||||
|  | 
 | ||||||
|  | describe('The HttpError class', () => { | ||||||
|  |     test('should allow passing a custom message', () => { | ||||||
|  |         const status = Math.random() | ||||||
|  |         const reason = 'testing' | ||||||
|  | 
 | ||||||
|  |         const error = new HttpError(status, reason) | ||||||
|  | 
 | ||||||
|  |         expect(error.toString()).toBe(`HttpError: ${status} - ${reason}`) | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('should use the default reason for the status when passing no second arg', () => { | ||||||
|  |         // ts will always consider it a string
 | ||||||
|  |         for (let untypedStatus in HTTP_REASONS) { | ||||||
|  |             // this forces ts to belive its an actual status
 | ||||||
|  |             const status = (untypedStatus as unknown) as HttpStatus | ||||||
|  |             const error = new HttpError(status) | ||||||
|  | 
 | ||||||
|  |             expect(error.reason).toBe(HTTP_REASONS[status]) | ||||||
|  |         } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     test('should always have the http error symbol set to true', () => { | ||||||
|  |         const error = new HttpError() | ||||||
|  | 
 | ||||||
|  |         expect(error[httpSymbol]).toBe(true) | ||||||
|  |     }) | ||||||
|  | }) | ||||||
|  | @ -1,6 +1,11 @@ | ||||||
| import { Middleware } from 'koa' | import { Middleware } from 'koa' | ||||||
| import { httpSymbol } from '../../network/classes/HttpError' | import { httpSymbol } from '../../network/classes/HttpError' | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Midlware for error handling | ||||||
|  |  * | ||||||
|  |  * Not testing it because its made by Enitoni | ||||||
|  |  */ | ||||||
| export const handleError = (): Middleware => async (context, next) => { | export const handleError = (): Middleware => async (context, next) => { | ||||||
|     try { |     try { | ||||||
|         await next() |         await next() | ||||||
|  |  | ||||||
|  | @ -1,13 +1,18 @@ | ||||||
| import Koa, { Middleware } from 'koa' | import Koa, { Middleware } from 'koa' | ||||||
| import session from 'koa-session' | import session from 'koa-session' | ||||||
| import knexSessionStore from 'koa-session-knex-store' | import knexSessionStore from 'koa-session-knex-store' | ||||||
| import { DbManager } from '../../db/classes/DbManager' | import { connection } from '../../db/connection' | ||||||
| 
 | 
 | ||||||
| const { connection } = new DbManager() | // The store sessions are saved to
 | ||||||
| const store = knexSessionStore(connection, { | const store = knexSessionStore(connection, { | ||||||
|     createtable: true |     createtable: true | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Middleware factory for handling sessions | ||||||
|  |  * | ||||||
|  |  * @param app The app to handle sessions for | ||||||
|  |  */ | ||||||
| export const handleSessions = (app: Koa): Middleware => | export const handleSessions = (app: Koa): Middleware => | ||||||
|     session( |     session( | ||||||
|         { |         { | ||||||
|  |  | ||||||
|  | @ -1,15 +0,0 @@ | ||||||
| import { Middleware } from 'koa' |  | ||||||
| import { HttpError } from '../classes/HttpError' |  | ||||||
| 
 |  | ||||||
| export const hasFields = ( |  | ||||||
|     ...fields: string[] |  | ||||||
| ): Middleware<{ field: string }> => async (context, next) => { |  | ||||||
|     for (const value of fields) { |  | ||||||
|         if (context.request.body[value]) { |  | ||||||
|             context.state.field = value |  | ||||||
|             return next() |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     throw new HttpError(400, `None of the fields ${fields.join(' ')} included.`) |  | ||||||
| } |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Mateiadrielrafael
				Mateiadrielrafael