1
Fork 0

typescript(lunargame/api): added eslint

Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
Mateiadrielrafael 2019-08-12 21:12:56 +03:00 committed by prescientmoon
parent 013b17dc22
commit 02bf17d559
Signed by: prescientmoon
SSH key fingerprint: SHA256:UUF9JT2s8Xfyv76b8ZuVL7XrmimH4o49p4b+iexbVH4
30 changed files with 1345 additions and 54 deletions

View file

@ -0,0 +1,25 @@
{
"parser": "@typescript-eslint/parser",
"env": {
"es6": true,
"node": true,
"jest": true
},
"extends": [
"plugin:@typescript-eslint/recommended",
"prettier/@typescript-eslint",
"plugin:prettier/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module"
},
"rules": {
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-object-literal-type-assertion": 0
}
}

View file

@ -1,7 +1,7 @@
{ {
"trailingComma": "none", "trailingComma": "none",
"singleQuote": true, "singleQuote": true,
"printWidth": 80, "printWidth": 100,
"tabWidth": 4, "tabWidth": 4,
"semi": false "semi": false
} }

View file

@ -2,5 +2,7 @@
"eslint.enable": true, "eslint.enable": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"prettier.eslintIntegration": true, "prettier.eslintIntegration": true,
"explorer.autoReveal": false "explorer.autoReveal": false,
"eslint.autoFixOnSave": true,
"eslint.validate": ["javascript", { "language": "typescript", "autoFix": true }]
} }

View file

@ -16,8 +16,8 @@ exports.up = knex => {
// the password of the user // the password of the user
table.text('password').notNullable() table.text('password').notNullable()
// the password encription // the password encryption
table.text('password_encription').notNullable() table.text('passwordEncryption').notNullable()
}) })
} }

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@
"devDependencies": { "devDependencies": {
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/dotenv": "^6.1.1", "@types/dotenv": "^6.1.1",
"@types/faker": "^4.1.5",
"@types/jest": "^24.0.17", "@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",
@ -20,12 +21,18 @@
"@types/koa-session": "^5.10.1", "@types/koa-session": "^5.10.1",
"@types/koa__cors": "^2.2.3", "@types/koa__cors": "^2.2.3",
"@types/node": "^12.0.10", "@types/node": "^12.0.10",
"@types/nodemailer": "^6.2.0",
"@types/supertest": "^2.0.8", "@types/supertest": "^2.0.8",
"@types/uuid": "^3.4.5", "@types/uuid": "^3.4.5",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/parser": "^1.13.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"eslint": "^6.1.0",
"eslint-config-prettier": "^6.0.0",
"eslint-plugin-prettier": "^3.1.0",
"faker": "^4.1.0",
"jest": "^24.8.0", "jest": "^24.8.0",
"nodemon": "^1.19.1", "nodemon": "^1.19.1",
"prettier": "^1.18.2",
"sqlite3": "^4.0.9", "sqlite3": "^4.0.9",
"ts-jest": "^24.0.2", "ts-jest": "^24.0.2",
"ts-node": "^8.3.0", "ts-node": "^8.3.0",
@ -37,13 +44,13 @@
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"dotenv": "^8.0.0", "dotenv": "^8.0.0",
"joi": "^14.3.1", "joi": "^14.3.1",
"joi-extract-type": "^15.0.0",
"knex": "^0.18.1", "knex": "^0.18.1",
"koa": "^2.7.0", "koa": "^2.7.0",
"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",
"koa-session-knex-store": "^1.1.2", "koa-session-knex-store": "^1.1.2",
"nodemailer": "^6.2.1",
"pg": "^7.11.0", "pg": "^7.11.0",
"supertest": "^4.0.2", "supertest": "^4.0.2",
"uuid": "^3.3.2" "uuid": "^3.3.2"

View file

@ -8,15 +8,8 @@ describe('The randomElement function', () => {
}) })
test('should throw an error when passing an empty array', () => { test('should throw an error when passing an empty array', () => {
let error: Error | undefined expect(() => {
try {
randomElement([]) randomElement([])
} catch (catchedError) { }).toThrow()
//
error = catchedError
}
expect(error).toBeTruthy()
}) })
}) })

View file

@ -0,0 +1,4 @@
import { passwordEncryption } from './types/passwordEncryption'
// i made a separate constant to prevent duplication
export const defaultEncryptionMethod: passwordEncryption = 'bcrypt'

View file

@ -0,0 +1,20 @@
import { checkPassword } from './checkPassword'
import { passwordEncryption } from '../types/passwordEncryption'
describe('The checkPassword helper', () => {
const pass = 'this is a test password'
test("should throw an error if the encryption method doesn't exist", () => {
expect(() => {
checkPassword(pass, pass, '12212' as passwordEncryption)
}).toThrow()
})
test('should return true if the password matches the hash and the encryption = plain', () => {
expect(checkPassword(pass, pass, 'plain')).toBe(true)
})
test('shoud return false if the password is wrong and the encryption = plain', () => {
expect(checkPassword(pass, pass + 'something', 'plain')).toBe(false)
})
})

View file

@ -0,0 +1,21 @@
import { passwordEncryption } from '../types/passwordEncryption'
import { HttpError } from '../../network/classes/HttpError'
/**
* Comparesa apssword with it's hash
*
* @param hash The hash of the password
* @param password The actual password
* @param encryption The encription of the password
*/
export const checkPassword = (
hash: string,
password: string,
encryption: passwordEncryption = 'plain'
) => {
if (encryption === 'plain') {
return hash === password
} else {
throw new HttpError(400, `Encription ${encryption} doesn't exist`)
}
}

View file

@ -0,0 +1,22 @@
import { internet } from 'faker'
import { encryptPassword } from './encryptPassword'
import { compare } from 'bcryptjs'
describe('The encryptPassword helper', () => {
test("should return the same password if the method is 'plain'", async () => {
const password = internet.password()
const hash = await encryptPassword(password, 'plain')
expect(hash).toBe(password)
})
test("should return a mactching hash if the method is 'bcrypt'", async () => {
const password = internet.password()
// the amount of rounds is small because this is just a test
const hash = await encryptPassword(password, 'bcrypt', 3)
const match = await compare(password, hash)
expect(match).toBe(true)
})
})

View file

@ -0,0 +1,24 @@
import { passwordEncryption } from '../types/passwordEncryption'
import { genSalt, hash } from 'bcryptjs'
/**
* Encypts a string
*
* @param password The password to encrypt
* @param method The method to encrypt the password with
* @param rounds The salting rounds (for bcrypt only)
*/
export const encryptPassword = async (
password: string,
method: passwordEncryption,
rounds = 10
) => {
if (method === 'bcrypt') {
const salt = await genSalt(rounds)
const result = await hash(password, salt)
return result
} else {
return password
}
}

View file

@ -0,0 +1,13 @@
import { Middleware } from 'koa'
import { HttpError } from '../../network/classes/HttpError'
/**
* Middlware wich throws an error if the user isn't logged in
*/
export const isAuthorized = (): Middleware => (context, next) => {
if (context.session.uid !== undefined) {
return next()
} else {
throw new HttpError(401)
}
}

View file

@ -0,0 +1,28 @@
import { Context } from 'koa'
import { isUnauthorized } from './isUnauthorized'
describe('The isUnauthorized middleware', () => {
const fakeNext = () => async () => {}
test('should throw an error if the user is logged in', () => {
const fakeContext = ({
session: {
uid: 7
}
} as unknown) as Context
expect(() => isUnauthorized()(fakeContext, fakeNext())).toThrow()
})
test("should call next if the user isn't logged in", () => {
const fakeContext = {
session: {}
} as Context
const next = jest.fn(fakeNext())
isUnauthorized()(fakeContext, next)
expect(next).toBeCalled()
})
})

View file

@ -0,0 +1,13 @@
import { Middleware } from 'koa'
import { HttpError } from '../../network/classes/HttpError'
/**
* Middleware wich throws an error if the user is logged in
*/
export const isUnauthorized = (): Middleware => (context, next) => {
if (context.session.uid === undefined) {
return next()
} else {
throw new HttpError(401)
}
}

View file

@ -0,0 +1,28 @@
import { isAuthorized } from './isAuthorized'
import { Context } from 'koa'
describe('The isAuthorized middleware', () => {
const fakeNext = () => async () => {}
test("should throw an error if the user isn't logged in", () => {
const fakeContext = {
session: {}
} as Context
expect(() => isAuthorized()(fakeContext, fakeNext())).toThrow()
})
test('should call next if the user is logged in', () => {
const fakeContext = ({
session: {
uid: Math.random()
}
} as unknown) as Context
const next = jest.fn(fakeNext())
isAuthorized()(fakeContext, next)
expect(next).toBeCalled()
})
})

View file

@ -0,0 +1,32 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { name, random, internet } from 'faker'
import { createAccount } from './createAccount'
import { connection } from '../../db/connection'
import { SignupBody } from '../schemas/SignupBody'
describe('The createAccount query', () => {
test('should return the id of the account and add it to the db', async () => {
const email = internet.email()
const username = name.firstName()
const password = random.alphaNumeric(10)
const result = await createAccount({
email,
name: username,
password,
passwordEncryption: 'plain'
})
const account = await connection
.from('account')
.select<Required<SignupBody>>(['email', 'name', 'password'])
.where({
id: result
})
.first()
expect(account.name).toBe(username)
expect(account.email).toBe(email)
expect(account.password).toBe(password)
})
})

View file

@ -0,0 +1,15 @@
import { connection } from '../../db/connection'
import { DbAccount } from '../types/Account'
/**
* Saves a new user into the db
*
* @param user The user object to insert
*/
export const createAccount = async (user: DbAccount): Promise<number> => {
const result = await connection.from('account').insert({
...user
})
return result[0]
}

View file

@ -0,0 +1,16 @@
import { getPasswordByEmail } from './getPasswordByEmail'
import { mockAccounts } from '../../../../test/seeds/01_create-account'
import { connection } from '../../db/connection'
describe('The getPasswordByName query', () => {
test('should return the correct password & encryption for a mock account', async () => {
await connection.seed.run()
for (const account of mockAccounts) {
const result = await getPasswordByEmail(account.email)
expect(result.password).toBe(account.password)
expect(result.passwordEncryption).toBe(account.passwordEncryption)
}
})
})

View file

@ -0,0 +1,26 @@
import { connection } from '../../db/connection'
import { passwordEncryption } from '../types/passwordEncryption'
/**
* The result of the getPasswordByName query
*/
export interface PasswordByEmailResult {
password: string
passwordEncryption: passwordEncryption
id: number
}
/**
* Gets the password, passwordEncryption and id of an account from it's email
*
* @param email The email of the account
*/
export const getPasswordByEmail = (email: string): Promise<PasswordByEmailResult> => {
return connection
.from('account')
.select('password', 'passwordEncryption', 'id')
.where({
email
})
.first()
}

View file

@ -1,26 +1,95 @@
import supertest from 'supertest' import supertest from 'supertest'
import { app } from '../../../server' import { app } from '../../../server'
import { loggedInAgent } from '../../../../test/utils/loggedInAgent' import { loggedInAgent } from '../../../../test/utils/loggedInAgent'
import { mockAccounts } from '../../../../test/seeds/01_create-account'
import { random, internet } from 'faker'
import { LoginReponseBody } from '../types/LoginReponseBody'
import { defaultEncryptionMethod } from '../constants'
describe('The /auth route', () => { describe('The /auth route', () => {
// used to make requests
let request = supertest(app.callback()) let request = supertest(app.callback())
describe(`The POST method on the /login subroute`, () => {
test('should throw an error if the password field is empty', async () => {
const response = await request.post('/auth/login').send({
name: mockAccounts[0].name
})
expect(response.status).not.toBe(200)
})
test('should throw an error if the name field is empty', async () => {
const response = await request.post('/auth/login').send({
password: mockAccounts[0].password
})
expect(response.status).not.toBe(200)
})
test('should throw an error if the password is wrong', async () => {
const response = await request.post('/auth/login').send({
name: mockAccounts[0].name,
password: mockAccounts[0].password + 'something'
})
expect(response.status).not.toBe(200)
})
test('should work just fine when the password is correct', async () => {
for (const account of mockAccounts) {
const response = await request.post('/auth/login').send({
email: account.email,
password: account.password
})
// i'm making a separate constant for vsc to help me
const body: LoginReponseBody = response.body
expect(response.status).toBe(200)
expect(body.uid).not.toBe(undefined)
expect(body.encryption).toBe(account.passwordEncryption)
}
})
})
describe(`The GET method on the / subroute`, () => {
test('should return undefined if the user was not logged in', async () => { test('should return undefined if the user was not logged in', async () => {
const res = await request.get('/auth') const res = await request.get('/auth')
expect(res.body.uid).toBe(undefined) expect(res.body.uid).toBe(undefined)
}) })
test.only('should return the uid form the session while logged in', async () => { test('should return the uid form the session while logged in', async () => {
const uid = 7 const [agent, cookie] = await loggedInAgent(supertest.agent(app.callback()), {
email: mockAccounts[0].email,
password: mockAccounts[0].password
})
const [agent, cookie] = await loggedInAgent( const response = await agent.get('/auth').set('cookie', cookie)
supertest.agent(app.callback()),
uid
)
const res = await agent.get('/auth').set('cookie', cookie) expect(response.body.uid).not.toBe(undefined)
})
})
expect(res.body.uid).toBe(uid) describe('The POST method on the /signup subroute', () => {
test('should return the email name and the encrytion', async () => {
const username = random.alphaNumeric(7)
const password = random.alphaNumeric(5)
const email = internet.email()
const response = await request.post('/auth/signup').send({
name: username,
email,
password
})
// i'm making a separate constant for vsc to help me
const body: LoginReponseBody = response.body
expect(response.status).toBe(200)
expect(body.uid).not.toBe(undefined)
expect(body.encryption).toBe(defaultEncryptionMethod)
})
}) })
}) })

View file

@ -1,4 +1,14 @@
import Router from 'koa-router' import Router from 'koa-router'
import { validate } from '../../../common/validation/middleware/validate'
import { getPasswordByEmail } from '../queries/getPasswordByEmail'
import { HttpError } from '../../network/classes/HttpError'
import { checkPassword } from '../helpers/checkPassword'
import { SignupBodySchema } from '../schemas/SignupBody'
import { encryptPassword } from '../helpers/encryptPassword'
import { createAccount } from '../queries/createAccount'
import { defaultEncryptionMethod } from '../constants'
import { LoginBodySchema } from '../schemas/LoginBody'
import { isUnauthorized } from '../middleware/isUnauthorized'
const router = new Router() const router = new Router()
@ -10,11 +20,65 @@ router.get('/', (context, next) => {
return next() return next()
}) })
router.post('/login', (context, next) => { router.post(
context.session.uid = context.request.body.uid '/login',
context.body = {} isUnauthorized(),
validate(LoginBodySchema, 'body'),
async (context, next) => {
const { email, password } = context.request.body
const passwordData = await getPasswordByEmail(email)
// in case the user doesnt exist
if (!passwordData) {
throw new HttpError(400)
}
const match = checkPassword(
passwordData.password,
password,
passwordData.passwordEncryption
)
if (!match) {
throw new HttpError(400, 'wrong password')
}
context.session.uid = passwordData.id
context.body = {
encryption: passwordData.passwordEncryption,
uid: passwordData.id
}
return next() return next()
}) }
)
router.post(
'/signup',
isUnauthorized(),
validate(SignupBodySchema, 'body'),
async (context, next) => {
const { email, name, password } = context.request.body
// encript the password (bcrypt by default)
const encryptedPassword = await encryptPassword(password, defaultEncryptionMethod, 10)
const uid = await createAccount({
email,
name,
password: encryptedPassword,
passwordEncryption: defaultEncryptionMethod
})
context.body = {
uid,
encryption: defaultEncryptionMethod
}
return next()
}
)
export default router export default router

View file

@ -1,7 +1,9 @@
import Joi from 'joi' import Joi from '@hapi/joi'
import { name, password } from './authFields' import { email, password } from './authFields'
export const LoginBodySchema = Joi.object({ export const LoginBodySchema = Joi.object({
name, email,
password password
}) }).required()
export type LoginBody = Joi.extractType<typeof LoginBodySchema>

View file

@ -0,0 +1,10 @@
import Joi from '@hapi/joi'
import { email, name, password } from './authFields'
export const SignupBodySchema = Joi.object({
name,
password,
email
}).required()
export type SignupBody = Joi.extractType<typeof SignupBodySchema>

View file

@ -1,10 +1,8 @@
import Joi from 'joi' import Joi from 'joi'
export const name = Joi.string() export const name = Joi.string()
.alphanum()
.min(3) .min(3)
.max(30) .max(30)
.lowercase()
.required() .required()
export const email = Joi.string() export const email = Joi.string()
@ -15,6 +13,6 @@ export const email = Joi.string()
export const password = Joi.string() export const password = Joi.string()
.min(3) .min(3)
.max(50) .max(20)
.alphanum() .alphanum()
.required() .required()

View file

@ -0,0 +1,32 @@
import { passwordEncryption } from './passwordEncryption'
/**
* The data about an account wich needs to be inserted into the db
*/
export interface DbAccount {
name: string
email: string
password: string
passwordEncryption: passwordEncryption
}
/**
* The data about an account wich actually gets stored into the db
*/
export interface FullDbAccount extends DbAccount {
id: number
}
/**
* The data everyone can get about an account
*/
export interface AccountPublicData {
name: string
}
/**
* The data only the owner of the account has acces to
*/
export interface AccountPrivateData extends AccountPublicData {
email: string
}

View file

@ -0,0 +1,6 @@
import { passwordEncryption } from './passwordEncryption'
export interface LoginReponseBody {
uid: number
encryption: passwordEncryption
}

View file

@ -0,0 +1,4 @@
/**
* All modes a password can be encrypted in
*/
export type passwordEncryption = 'plain' | 'bcrypt'

View file

@ -0,0 +1,21 @@
import * as Knex from 'knex'
import { DbAccount } from '../../src/modules/auth/types/Account'
const tableName = 'account'
export const mockAccounts: DbAccount[] = [
{
name: 'Adriel',
email: 'rafaeladriel11@gmail.com',
password: '1234',
passwordEncryption: 'plain'
}
]
export async function seed(knex: Knex): Promise<any> {
return knex(tableName)
.del()
.then(() => {
return knex(tableName).insert(mockAccounts)
})
}

View file

@ -1,4 +1,6 @@
import supertest from 'supertest' import supertest from 'supertest'
import 'joi-extract-type'
import { LoginBody } from '../../src/modules/auth/schemas/LoginBody'
/** /**
* Helper to get a supertest agent wich is logged in * Helper to get a supertest agent wich is logged in
@ -8,10 +10,11 @@ import supertest from 'supertest'
*/ */
export const loggedInAgent = async ( export const loggedInAgent = async (
agent: supertest.SuperTest<supertest.Test>, agent: supertest.SuperTest<supertest.Test>,
uid: number { email, password }: LoginBody
) => { ) => {
const response = await agent.post('/auth/login').send({ const response = await agent.post('/auth/login').send({
uid email,
password
}) })
// the cookie to send back // the cookie to send back