typescript(lunargame/api): initial commit
Signed-off-by: prescientmoon <git@moonythm.dev>
This commit is contained in:
commit
175cea413f
2
typescript/lunargame/api/.gitignore
vendored
Normal file
2
typescript/lunargame/api/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
node_modules
|
||||||
|
.env
|
7
typescript/lunargame/api/.prettierrc.json
Normal file
7
typescript/lunargame/api/.prettierrc.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"singleQuote": true,
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false
|
||||||
|
}
|
6
typescript/lunargame/api/.vscode/settings.json
vendored
Normal file
6
typescript/lunargame/api/.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"eslint.enable": true,
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"prettier.eslintIntegration": true,
|
||||||
|
"explorer.autoReveal": false
|
||||||
|
}
|
17
typescript/lunargame/api/knexfile.ts
Normal file
17
typescript/lunargame/api/knexfile.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
export const development = {
|
||||||
|
client: 'pg',
|
||||||
|
connection: {
|
||||||
|
port: '5432',
|
||||||
|
host: 'localhost',
|
||||||
|
database: 'lunarbox',
|
||||||
|
user: 'postgres',
|
||||||
|
password: 'drielrafael11'
|
||||||
|
},
|
||||||
|
migrations: {
|
||||||
|
directory: './migrations',
|
||||||
|
tablename: 'migrations'
|
||||||
|
},
|
||||||
|
seeds: {
|
||||||
|
directory: './seeds'
|
||||||
|
}
|
||||||
|
}
|
24
typescript/lunargame/api/migrations/create-user.js
Normal file
24
typescript/lunargame/api/migrations/create-user.js
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
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')
|
||||||
|
}
|
29
typescript/lunargame/api/migrations/create_game.js
Normal file
29
typescript/lunargame/api/migrations/create_game.js
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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')
|
||||||
|
}
|
15
typescript/lunargame/api/migrations/create_password.js
Normal file
15
typescript/lunargame/api/migrations/create_password.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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')
|
||||||
|
}
|
6
typescript/lunargame/api/nodemon.json
Normal file
6
typescript/lunargame/api/nodemon.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"watch": ["src"],
|
||||||
|
"ext": "ts",
|
||||||
|
"ignore": ["src/**/*.spec.ts"],
|
||||||
|
"exec": "ts-node ./src/index.ts"
|
||||||
|
}
|
4010
typescript/lunargame/api/package-lock.json
generated
Normal file
4010
typescript/lunargame/api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
44
typescript/lunargame/api/package.json
Normal file
44
typescript/lunargame/api/package.json
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"start": "nodemon",
|
||||||
|
"reset:db": "knex migrate:rollback && knex migrate:latest && knex seed:run"
|
||||||
|
},
|
||||||
|
"main": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bcryptjs": "^2.4.2",
|
||||||
|
"@types/dotenv": "^6.1.1",
|
||||||
|
"@types/joi": "^14.3.3",
|
||||||
|
"@types/koa": "^2.0.49",
|
||||||
|
"@types/koa-bodyparser": "^4.3.0",
|
||||||
|
"@types/koa-router": "^7.0.42",
|
||||||
|
"@types/koa-session": "^5.10.1",
|
||||||
|
"@types/koa__cors": "^2.2.3",
|
||||||
|
"@types/node": "^12.0.10",
|
||||||
|
"@types/nodemailer": "^6.2.0",
|
||||||
|
"@types/uuid": "^3.4.5",
|
||||||
|
"nodemon": "^1.19.1",
|
||||||
|
"ts-node": "^8.3.0",
|
||||||
|
"typescript": "^3.5.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@eix/utils": "github:eix-js/utils",
|
||||||
|
"@koa/cors": "^3.0.0",
|
||||||
|
"@sendgrid/mail": "^6.4.0",
|
||||||
|
"bcryptjs": "^2.4.3",
|
||||||
|
"dotenv": "^8.0.0",
|
||||||
|
"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",
|
||||||
|
"koa-session-knex-store": "^1.1.2",
|
||||||
|
"nodemailer": "^6.2.1",
|
||||||
|
"pg": "^7.11.0",
|
||||||
|
"uuid": "^3.3.2"
|
||||||
|
}
|
||||||
|
}
|
19
typescript/lunargame/api/seeds/01_user.ts
Normal file
19
typescript/lunargame/api/seeds/01_user.ts
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
11
typescript/lunargame/api/seeds/02_password.ts
Normal file
11
typescript/lunargame/api/seeds/02_password.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
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 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
51
typescript/lunargame/api/seeds/03_game.ts
Normal file
51
typescript/lunargame/api/seeds/03_game.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
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,3 @@
|
||||||
|
export interface CountData {
|
||||||
|
count: string
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { ObjectSchema } from 'joi'
|
||||||
|
import { Middleware } from 'koa'
|
||||||
|
import { HttpError } from '../../../modules/network/classes/HttpError'
|
||||||
|
|
||||||
|
export const validate = (
|
||||||
|
schema: ObjectSchema,
|
||||||
|
field: 'params' | 'body' | 'query'
|
||||||
|
): Middleware => async (context, next) => {
|
||||||
|
const result = schema.validate(
|
||||||
|
field === 'body' ? context.request.body : context[field]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result.error) throw new HttpError(400, result.error.message)
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
29
typescript/lunargame/api/src/index.ts
Normal file
29
typescript/lunargame/api/src/index.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import Koa from 'koa'
|
||||||
|
import cors from '@koa/cors'
|
||||||
|
import parser from 'koa-bodyparser'
|
||||||
|
|
||||||
|
import { config } from 'dotenv'
|
||||||
|
import { handleError } from './modules/network/middleware/errorHandler'
|
||||||
|
import { handleSessions } from './modules/network/middleware/handleSessions'
|
||||||
|
import { router } from './modules/core/router'
|
||||||
|
|
||||||
|
config()
|
||||||
|
|
||||||
|
const port = process.env.PORT
|
||||||
|
const app = new Koa()
|
||||||
|
|
||||||
|
app.keys = [process.env.secret || 'secret']
|
||||||
|
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
credentials: true
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.use(handleError())
|
||||||
|
.use(handleSessions(app))
|
||||||
|
.use(parser())
|
||||||
|
.use(router.middleware())
|
||||||
|
|
||||||
|
app.listen(Number(port), () => {
|
||||||
|
console.log(`Listening on port ${port}`)
|
||||||
|
})
|
29
typescript/lunargame/api/src/modules/auth/authSchemas.ts
Normal file
29
typescript/lunargame/api/src/modules/auth/authSchemas.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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
|
||||||
|
})
|
15
typescript/lunargame/api/src/modules/auth/constants.ts
Normal file
15
typescript/lunargame/api/src/modules/auth/constants.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
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'
|
||||||
|
]
|
|
@ -0,0 +1,17 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
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}
|
||||||
|
`
|
|
@ -0,0 +1,9 @@
|
||||||
|
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.')
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
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()
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
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.')
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
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('*')
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
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('*')
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
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()
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
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()
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
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()
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
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()
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
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')
|
||||||
|
}
|
59
typescript/lunargame/api/src/modules/auth/routes/account.ts
Normal file
59
typescript/lunargame/api/src/modules/auth/routes/account.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
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 }
|
75
typescript/lunargame/api/src/modules/auth/routes/auth.ts
Normal file
75
typescript/lunargame/api/src/modules/auth/routes/auth.ts
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
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 }
|
26
typescript/lunargame/api/src/modules/auth/routes/user.ts
Normal file
26
typescript/lunargame/api/src/modules/auth/routes/user.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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 }
|
|
@ -0,0 +1,9 @@
|
||||||
|
export interface Account {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
avatar: string
|
||||||
|
description: string
|
||||||
|
uid: string
|
||||||
|
verified: boolean
|
||||||
|
verificationToken: string
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
export interface Password {
|
||||||
|
uid: string
|
||||||
|
value: string
|
||||||
|
secure: boolean
|
||||||
|
}
|
4
typescript/lunargame/api/src/modules/core/constants.ts
Normal file
4
typescript/lunargame/api/src/modules/core/constants.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
type processMode = 'development' | 'production' | 'test'
|
||||||
|
|
||||||
|
export const mode: processMode =
|
||||||
|
(process.env.NODE_ENV as processMode) || 'development'
|
14
typescript/lunargame/api/src/modules/core/router.ts
Normal file
14
typescript/lunargame/api/src/modules/core/router.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
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())
|
||||||
|
|
||||||
|
export { router }
|
|
@ -0,0 +1,9 @@
|
||||||
|
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])
|
||||||
|
}
|
13
typescript/lunargame/api/src/modules/game/gameSchemas.ts
Normal file
13
typescript/lunargame/api/src/modules/game/gameSchemas.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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
|
||||||
|
})
|
|
@ -0,0 +1,13 @@
|
||||||
|
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)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
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)
|
||||||
|
}
|
29
typescript/lunargame/api/src/modules/game/routes/game.ts
Normal file
29
typescript/lunargame/api/src/modules/game/routes/game.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
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 }
|
|
@ -0,0 +1,24 @@
|
||||||
|
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,48 @@
|
||||||
|
/**
|
||||||
|
* Made by Entioni
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum HttpStatus {
|
||||||
|
BadRequest = 400,
|
||||||
|
Unauthorized = 401,
|
||||||
|
PaymentRequired = 402,
|
||||||
|
Forbidden = 403,
|
||||||
|
NotFound = 404,
|
||||||
|
Conflict = 409,
|
||||||
|
Gone = 410,
|
||||||
|
PayloadTooLarge = 413,
|
||||||
|
TooManyRequests = 429,
|
||||||
|
InternalServerError = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HTTP_REASONS: Record<HttpStatus, string> = {
|
||||||
|
'400': 'Bad request',
|
||||||
|
'401': 'Unauthorized',
|
||||||
|
'402': 'Payment required',
|
||||||
|
'403': 'Forbidden',
|
||||||
|
'404': 'Not found',
|
||||||
|
'409': 'Conflict',
|
||||||
|
'410': 'Gone',
|
||||||
|
'413': 'Payload too large',
|
||||||
|
'429': 'Too many requests',
|
||||||
|
'500': 'Internal server error'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const httpSymbol = Symbol('http')
|
||||||
|
|
||||||
|
export class HttpError extends Error {
|
||||||
|
// for some reason instanceof stopped working at some point
|
||||||
|
public [httpSymbol] = true
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public status: HttpStatus = HttpStatus.InternalServerError,
|
||||||
|
public reason?: string
|
||||||
|
) {
|
||||||
|
super()
|
||||||
|
this.reason = reason || HTTP_REASONS[status]
|
||||||
|
}
|
||||||
|
|
||||||
|
public toString() {
|
||||||
|
return `HttpError: ${this.status} - ${this.reason}`
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Middleware } from 'koa'
|
||||||
|
import { httpSymbol } from '../../network/classes/HttpError'
|
||||||
|
|
||||||
|
export const handleError = (): Middleware => async (context, next) => {
|
||||||
|
try {
|
||||||
|
await next()
|
||||||
|
} catch (error) {
|
||||||
|
if (error[httpSymbol]) {
|
||||||
|
context.status = error.status
|
||||||
|
context.body = {
|
||||||
|
message: error.reason
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(error)
|
||||||
|
|
||||||
|
context.status = 500
|
||||||
|
context.body = 'Internal server error'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
import Koa, { Middleware } from 'koa'
|
||||||
|
import session from 'koa-session'
|
||||||
|
import knexSessionStore from 'koa-session-knex-store'
|
||||||
|
import { DbManager } from '../../db/classes/DbManager'
|
||||||
|
|
||||||
|
const { connection } = new DbManager()
|
||||||
|
const store = knexSessionStore(connection, {
|
||||||
|
createtable: true
|
||||||
|
})
|
||||||
|
|
||||||
|
export const handleSessions = (app: Koa): Middleware =>
|
||||||
|
session(
|
||||||
|
{
|
||||||
|
key: 'something',
|
||||||
|
maxAge: 1000 * 60 * 60 * (24 * 7),
|
||||||
|
overwrite: true,
|
||||||
|
signed: true,
|
||||||
|
rolling: true,
|
||||||
|
renew: false,
|
||||||
|
store,
|
||||||
|
domain: 'localhost'
|
||||||
|
},
|
||||||
|
app
|
||||||
|
)
|
|
@ -0,0 +1,15 @@
|
||||||
|
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.`)
|
||||||
|
}
|
13
typescript/lunargame/api/tsconfig.json
Normal file
13
typescript/lunargame/api/tsconfig.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"lib": ["es2015", "dom", "dom.iterable", "esnext"],
|
||||||
|
"target": "esnext"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
Loading…
Reference in a new issue