Skip to content

Commit

Permalink
chore: user changes (#10)
Browse files Browse the repository at this point in the history
* chore: user changes

* feat: add basic auth to GET. check for old and new entries

* chore: update env example

* fix: user check

* chore: update error msgs
  • Loading branch information
nicosantangelo authored and abarmat committed Mar 19, 2019
1 parent 59f5b8e commit 3891de3
Show file tree
Hide file tree
Showing 14 changed files with 187 additions and 32 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ SERVER_PORT=
CORS_ORIGIN=
CORS_METHOD=

AUTH_USERNAME=
AUTH_PASSWORD=

SERVER_SECRET=

AWS_ACCESS_KEY=
Expand Down
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"decentraland-commons": "^5.1.0",
"decentraland-server": "^2.0.1",
"express": "^4.16.4",
"express-basic-auth": "^1.1.6",
"tslint": "^5.12.1",
"typescript": "^3.3.3"
},
Expand Down
36 changes: 36 additions & 0 deletions src/Contest/AuthContest.router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import express = require('express')
import { server } from 'decentraland-server'

import { AuthRouter } from '../common'
import { readFile, parseFileBody } from '../S3'
import { decrypt } from '../crypto'
import { Entry } from './types'

export class AuthContestRouter extends AuthRouter {
mount() {
/**
* Get entry by id
*/
this.router.get('/entry/:projectId', server.handleRequest(this.getEntry))
}

async getEntry(req: express.Request): Promise<Entry> {
const projectId = server.extractFromReq(req, 'projectId')
let entry: Entry

try {
const file = await readFile(projectId)
entry = parseFileBody(file)
} catch (error) {
throw new Error(`Unknown entry ${projectId}`)
}

entry.contest.email = await decrypt(entry.contest.email)

if (entry.user) {
entry.user.id = await decrypt(entry.user.id)
}

return entry
}
}
53 changes: 31 additions & 22 deletions src/Contest/Contest.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,55 @@ import express = require('express')
import { server } from 'decentraland-server'

import { Router } from '../common'
import { uploadFile, readFile, checkFile } from '../S3'
import { uploadFile, readFile, checkFile, parseFileBody } from '../S3'
import { encrypt, decrypt } from '../crypto'
import { Entry } from './types'
import { LegacyEntry } from './types'
import { parseEntry } from './validations'

export class ContestRouter extends Router {
mount() {
/**
* Get entry by id
*/
this.router.get('/entry/:projectId', server.handleRequest(this.getEntry))

/**
* Upload a new entry
*/
this.router.post('/entry', server.handleRequest(this.submitProject))
}

async getEntry(req: express.Request): Promise<Entry> {
const projectId = server.extractFromReq(req, 'projectId')

const file = await readFile(projectId)

if (!file.Body) {
throw new Error(`Unknown entry ${projectId}`)
}

const entry: Entry = JSON.parse(file.Body.toString())
entry.contest.email = await decrypt(entry.contest.email)

return entry
}

async submitProject(req: express.Request): Promise<boolean> {
const EntryJSON = server.extractFromReq(req, 'entry')

const entry = parseEntry(EntryJSON)
const projectId = entry.project.id

let previousEntry: LegacyEntry | undefined

// We need to check if a previous entry exists and if it has an user,
// throw if it's different to the current entry's secret
try {
const file = await readFile(projectId)
previousEntry = parseFileBody(file)
} catch (error) {
// No previous entity
}

if (previousEntry) {
if (typeof previousEntry.user === 'undefined') {
// Handle old entries
const previousEmail = await decrypt(previousEntry.contest.email)
if (previousEmail !== entry.contest.email) {
throw new Error(
"You can't update this entry's email, please contact support"
)
}
} else {
const previousId = await decrypt(previousEntry.user.id)
if (previousId !== entry.user.id) {
throw new Error("New entry's secret doesn't match the previous one")
}
}
}

entry.contest.email = await encrypt(entry.contest.email)
entry.user.id = await encrypt(entry.user.id)

await uploadFile(projectId, Buffer.from(JSON.stringify(entry)))
await checkFile(projectId)
Expand Down
1 change: 1 addition & 0 deletions src/Contest/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Contest.router'
export * from './AuthContest.router'
14 changes: 13 additions & 1 deletion src/Contest/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Entry = {
export type BaseEntry = {
version: number
project: {
id: string
Expand All @@ -17,3 +17,15 @@ export type Entry = {
[key: string]: any
}
}

export type User = {
id: string
}

export type Entry = BaseEntry & {
user: User
}

export type LegacyEntry = BaseEntry & {
user?: User
}
23 changes: 18 additions & 5 deletions src/Contest/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ export function parseEntry(entryJSON: string): Entry {

switch (entry.version.toString()) {
case '1':
const { project, contest, scene } = entry
const { project, contest, scene, user } = entry

if (!user) {
throw new Error('You might be using an old version of the Builder.')
}
if (!project || !contest || !scene) {
throw new Error(
'Missing required props. Check your entry contains a project, contest and scene props'
Expand All @@ -18,7 +22,8 @@ export function parseEntry(entryJSON: string): Entry {
errors = [
getProjectErrors(project),
getContestErrors(contest),
getSceneErrors(scene)
getSceneErrors(scene),
getUserErrors(user)
]
break
default:
Expand Down Expand Up @@ -87,14 +92,22 @@ function getSceneErrors(scene: Entry['scene']): string {
return errors
}

function getUserErrors(user: Entry['user']): string {
const errors = validateProps(user, ['id'])
return errors.length > 0 ? `User:\n${formatErrors(errors)}` : ''
}

function formatErrors(errors: string[]): string {
return errors.map(error => `\t- ${error}`).join('')
}

export function validateProps(object: Object, props: string[]): string[] {
export function validateProps(
object: Record<string, any>,
props: string[]
): string[] {
const errors = []
for (const prop in props) {
if (typeof object === 'undefined') {
for (const prop of props) {
if (typeof object[prop] === 'undefined') {
errors.push(`Missing ${prop}`)
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/S3/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,9 @@ export function uploadFile(
}
return utils.promisify<AWS.S3.ManagedUpload>(s3.upload.bind(s3))(params)
}

export function parseFileBody(file: AWS.S3.GetObjectOutput): any | undefined {
if (file.Body) {
return JSON.parse(file.Body.toString())
}
}
51 changes: 51 additions & 0 deletions src/common/AuthRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import express = require('express')
import basicAuth = require('express-basic-auth')
import { env } from 'decentraland-commons'
import { PathParams, RequestHandlerParams } from 'express-serve-static-core'

import { Router } from './Router'
import { ExpressApp } from './ExpressApp'

export type Auth = {
username: string
password: string
}
type HTTPMethod = 'get' | 'post' | 'put' | 'delete'

export class AuthRouter extends Router {
protected username: string
protected password: string

constructor(router: ExpressApp | express.Router, auth: Auth) {
super(router)
this.username = auth.username
this.password = auth.password

if (!env.isDevelopment() && (!this.username || !this.password)) {
throw new Error(`Missing username or password in basic auth credentials`)
}

this.patchRouter()
}

patchRouter() {
const arr: HTTPMethod[] = ['get', 'post', 'put', 'delete']
const users = { [this.username]: this.password }

const authMiddleware =
this.username && this.password
? basicAuth({ users, challenge: true })
: (_: any, __: any, next: express.NextFunction) => next()

for (const method of arr) {
const oldHandler = this.router[method].bind(this.router)

this.router[method] = (
path: PathParams,
...handlers: RequestHandlerParams[]
) => {
return oldHandler(path, authMiddleware, ...handlers)
}
}
}
}
2 changes: 1 addition & 1 deletion src/common/ExpressApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export class ExpressApp {
return this
}

useVersion(version: string = '') {
useVersion(version: string) {
this.app.use(`/${version}`, this.router)
return this
}
Expand Down
1 change: 1 addition & 0 deletions src/common/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './Router'
export * from './AuthRouter'
4 changes: 2 additions & 2 deletions src/crypto/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function encrypt(text: string) {
if (!SECRET) {
return text
}
const key = await crypto.scryptSync(ALGORITHM, SECRET, KEY_SIZE)
const key = crypto.scryptSync(ALGORITHM, SECRET, KEY_SIZE)
const iv = crypto.randomBytes(16)
const cipher = crypto.createCipheriv(ALGORITHM, key, iv)
const crypted = cipher.update(text, 'utf8', 'hex')
Expand All @@ -24,7 +24,7 @@ export async function decrypt(encryptedText: string) {
if (!SECRET) {
return encryptedText
}
const key = await crypto.scryptSync(ALGORITHM, SECRET, KEY_SIZE)
const key = crypto.scryptSync(ALGORITHM, SECRET, KEY_SIZE)
const [text, iv] = extractIV(encryptedText)
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv)
const decrypted = decipher.update(text, 'hex', 'utf8')
Expand Down
8 changes: 7 additions & 1 deletion src/server.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { env } from 'decentraland-commons'
import { ContestRouter } from './Contest/Contest.router'
import { ContestRouter, AuthContestRouter } from './Contest'
import { ExpressApp } from './common/ExpressApp'

const SERVER_PORT = env.get('SERVER_PORT', '5000')
Expand All @@ -10,13 +10,19 @@ const app = new ExpressApp()
const corsOrigin = env.isDevelopment() ? '*' : env.get('CORS_ORIGIN', '')
const corsMethod = env.isDevelopment() ? '*' : env.get('CORS_METHOD', '')

const auth = {
username: env.get('AUTH_USERNAME', ''),
password: env.get('AUTH_PASSWORD', '')
}

app
.useJSON()
.useVersion(API_VERSION)
.useCORS(corsOrigin, corsMethod)

// Mount routers
new ContestRouter(app).mount()
new AuthContestRouter(app, auth).mount()

// Start
if (require.main === module) {
Expand Down

0 comments on commit 3891de3

Please sign in to comment.