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
							
								
								
									
										2
									
								
								typescript/lunargame/api/.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								typescript/lunargame/api/.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,2 +1,4 @@ | |||
| node_modules | ||||
| .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 = { | ||||
|     client: 'pg', | ||||
| import { iNode_env } from './src/modules/core/node_env' | ||||
| import { Config } from 'knex' | ||||
| import { resolve } from 'path' | ||||
| 
 | ||||
| // This is the name of the db file
 | ||||
| const dbName = 'db.sqlite' | ||||
| 
 | ||||
| // 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: { | ||||
|         port: '5432', | ||||
|         host: 'localhost', | ||||
|         database: 'lunarbox', | ||||
|         user: 'postgres', | ||||
|         password: 'drielrafael11' | ||||
|     }, | ||||
|     migrations: { | ||||
|         directory: './migrations', | ||||
|         tablename: 'migrations' | ||||
|             filename: resolve(dbFolder, dbName) | ||||
|         }, | ||||
|         migrations, | ||||
|         seeds: { | ||||
|         directory: './seeds' | ||||
|             directory: resolve(dbFolder, 'seeds') | ||||
|         } | ||||
|     }, | ||||
|     test: { | ||||
|         client: 'sqlite3', | ||||
|         connection: { | ||||
|             filename: resolve(testFolder, dbName) | ||||
|         }, | ||||
|         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", | ||||
|     "scripts": { | ||||
|         "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", | ||||
|     "private": true, | ||||
|     "devDependencies": { | ||||
|         "@types/bcryptjs": "^2.4.2", | ||||
|         "@types/dotenv": "^6.1.1", | ||||
|         "@types/jest": "^24.0.17", | ||||
|         "@types/joi": "^14.3.3", | ||||
|         "@types/koa": "^2.0.49", | ||||
|         "@types/koa-bodyparser": "^4.3.0", | ||||
|  | @ -19,7 +21,11 @@ | |||
|         "@types/node": "^12.0.10", | ||||
|         "@types/nodemailer": "^6.2.0", | ||||
|         "@types/uuid": "^3.4.5", | ||||
|         "cross-env": "^5.2.0", | ||||
|         "jest": "^24.8.0", | ||||
|         "nodemon": "^1.19.1", | ||||
|         "sqlite3": "^4.0.9", | ||||
|         "ts-jest": "^24.0.2", | ||||
|         "ts-node": "^8.3.0", | ||||
|         "typescript": "^3.5.2" | ||||
|     }, | ||||
|  | @ -32,7 +38,6 @@ | |||
|         "joi": "^14.3.1", | ||||
|         "knex": "^0.18.1", | ||||
|         "koa": "^2.7.0", | ||||
|         "koa-async-validator": "^0.4.1", | ||||
|         "koa-bodyparser": "^4.2.1", | ||||
|         "koa-router": "^7.4.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' | ||||
| 
 | ||||
| const router = new Router() | ||||
| 
 | ||||
| router.use('/account', accountRouter.middleware()) | ||||
| router.use('/auth', authRouter.middleware()) | ||||
| router.use('/user', userRouter.middleware()) | ||||
| router.use('/game', gameRouter.middleware()) | ||||
| router.use('/game') | ||||
| 
 | ||||
| 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 { httpSymbol } from '../../network/classes/HttpError' | ||||
| 
 | ||||
| /** | ||||
|  * Midlware for error handling | ||||
|  * | ||||
|  * Not testing it because its made by Enitoni | ||||
|  */ | ||||
| export const handleError = (): Middleware => async (context, next) => { | ||||
|     try { | ||||
|         await next() | ||||
|  |  | |||
|  | @ -1,13 +1,18 @@ | |||
| import Koa, { Middleware } from 'koa' | ||||
| import session from 'koa-session' | ||||
| 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, { | ||||
|     createtable: true | ||||
| }) | ||||
| 
 | ||||
| /** | ||||
|  * Middleware factory for handling sessions | ||||
|  * | ||||
|  * @param app The app to handle sessions for | ||||
|  */ | ||||
| export const handleSessions = (app: Koa): Middleware => | ||||
|     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