Skip to content

Commit

Permalink
feat: archive uploads (#957)
Browse files Browse the repository at this point in the history
  • Loading branch information
lautarodragan authored May 28, 2019
1 parent 5785a15 commit f47d130
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 13 deletions.
12 changes: 8 additions & 4 deletions src/api/API.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createModuleLogger, LoggingConfiguration } from '../utils/Logging/Loggi
import { SendEmailConfiguration } from '../utils/SendEmail'

import { AccountController } from '../controllers/AccountController'
import { ArchiveController } from '../controllers/ArchiveController'
import { errorHandling } from '../middlewares/errorHandling'
import { logger } from '../middlewares/logger'

Expand Down Expand Up @@ -40,7 +41,7 @@ interface APIMethods {
stop(): Promise<APIMethods>
}

const init = (accountController: AccountController) => ({
const init = (accountController: AccountController, archiveController: ArchiveController) => ({
maxApiRequestLimitForm,
maxApiRequestLimitJson,
passwordComplex,
Expand All @@ -51,7 +52,7 @@ const init = (accountController: AccountController) => ({
loggingConfiguration,
}: APIConfiguration) => {
const app = new Koa()
const route = routes(accountController)(
const route = routes(accountController, archiveController)(
passwordComplex,
sendEmail,
poetUrl,
Expand Down Expand Up @@ -93,13 +94,16 @@ const stopAPI = async (server: any, logger: Pino.Logger) => {
logger.info('Stopped API.')
}

export const API = (accountController: AccountController) => (configuration: APIConfiguration): APIMethods => {
export const API = (
accountController: AccountController,
archiveController: ArchiveController,
) => (configuration: APIConfiguration): APIMethods => {
const { loggingConfiguration } = configuration
const logger = createModuleLogger(loggingConfiguration)(__dirname)

return {
async start(): Promise<APIMethods> {
const app = init(accountController)(configuration)
const app = init(accountController, archiveController)(configuration)
this.server = await startAPI(app, configuration, logger)
return this
},
Expand Down
1 change: 1 addition & 0 deletions src/api/Path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ export enum Path {
TOKENS = '/tokens',
TOKENS_TOKENID = '/tokens/:tokenId',
HEALTH = '/health',
ARCHIVES = '/archives',
}
18 changes: 18 additions & 0 deletions src/api/archives/PostArchive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ArchiveController } from '../../controllers/ArchiveController'
import { IncorrectToken } from '../../errors/errors'
import { Token } from '../Tokens'

export const PostArchive = (archiveController: ArchiveController) => async (ctx: any, next: any): Promise<any> => {
const { req } = ctx.request
const { user, tokenData } = ctx.state

const allowedTokens = [Token.ApiKey.meta.name, Token.TestApiKey.meta.name]

if (!allowedTokens.includes(tokenData.data.meta.name))
throw new IncorrectToken(tokenData.data.meta.name, allowedTokens)

const response = await archiveController.postArchive(user, req, tokenData.data.meta.network)

ctx.status = 200
ctx.body = response
}
21 changes: 16 additions & 5 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import * as KoaRouter from 'koa-router'

import { AccountController } from '../controllers/AccountController'
import { authorization } from '../middlewares/authorization'
import { ArchiveController } from '../controllers/ArchiveController'

import { Authorization } from '../middlewares/authorization'
import { isLoggedIn } from '../middlewares/isLoggedIn'
import { monitor } from '../middlewares/monitor'
import { requireEmailVerified } from '../middlewares/requireEmailVerified'
Expand Down Expand Up @@ -36,14 +38,17 @@ import { CreateWork, CreateWorkSchema } from './works/CreateWork'
import { GetWork, GetWorkSchema } from './works/GetWork'
import { GetWorks } from './works/GetWorks'

export const routes = (accountController: AccountController) => (
import { PostArchive } from './archives/PostArchive'

export const routes = (accountController: AccountController, archiveController: ArchiveController) => (
passwordComplexConfiguration: PasswordComplexConfiguration,
sendEmailConfiguration: SendEmailConfiguration,
poetUrl: string,
maxApiTokens: number,
testPoetUrl: string,
) => {
const router = new KoaRouter()
const authorization = Authorization(accountController)

router.use([Path.WORKS, Path.WORKS_WORKID], (ctx: any, next: any) => {
ctx.set('Access-Control-Allow-Methods', 'POST,GET')
Expand All @@ -67,7 +72,7 @@ export const routes = (accountController: AccountController) => (
Path.TOKENS,
Path.TOKENS_TOKENID,
],
authorization(accountController),
authorization,
)

router.use([Path.WORKS, Path.WORKS_WORKID], requireEmailVerified())
Expand All @@ -94,13 +99,13 @@ export const routes = (accountController: AccountController) => (
)
router.patch(
Path.ACCOUNTS_ID,
authorization(accountController),
authorization,
validate(PatchAccountSchema),
PatchAccount(accountController),
)
router.post(
Path.ACCOUNTS_ID_POE_CHALLENGE,
authorization(accountController),
authorization,
validate(PostAccountPoeChallengeSchema),
PostAccountPoeChallenge(accountController),
)
Expand Down Expand Up @@ -137,5 +142,11 @@ export const routes = (accountController: AccountController) => (
router.get(Path.WORKS_WORKID, validate({ params: GetWorkSchema }), GetWork(poetUrl, testPoetUrl))
router.get(Path.WORKS, GetWorks(poetUrl, testPoetUrl))

router.post(
Path.ARCHIVES,
authorization,
PostArchive(archiveController),
)

return router
}
16 changes: 15 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import * as Pino from 'pino'
import { API } from './api/API'
import { Configuration } from './configuration'
import { AccountController } from './controllers/AccountController'
import { ArchiveController } from './controllers/ArchiveController'
import { AccountDao } from './daos/AccountDao'
import { PoetNode } from './daos/PoetNodeDao'
import { initVault } from './initVault'
import { loadConfigurationWithDefaults } from './loadConfiguration'
import { loggingConfigurationToPinoConfiguration } from './utils/Logging/Logging'
Expand Down Expand Up @@ -59,7 +61,19 @@ export async function app(localVars: any = {}) {
},
})

const frostAPI = await API(accountController)(configurationFrostAPI).start()
const archiveController = ArchiveController({
dependencies: {
logger: logger.child({ file: 'ArchiveController' }),
mainnetNode: PoetNode(configuration.poetUrl),
testnetNode: PoetNode(configuration.testPoetUrl),
},
configuration: {
poeContractAddress: configuration.poeContractAddress,
poeBalanceMinimum: configuration.poeBalanceMinimum,
},
})

const frostAPI = await API(accountController, archiveController)(configurationFrostAPI).start()

await accountDao.createIndices()

Expand Down
4 changes: 4 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ export interface Configuration
readonly transactionalMandrill: string
readonly jwt: string
readonly skipVault: boolean
readonly poeContractAddress: string
readonly poeBalanceMinimum: number
}

export const configuration: Configuration = {
Expand Down Expand Up @@ -107,4 +109,6 @@ export const configuration: Configuration = {
maildevIgnoreTLS: true,
loggingLevel: 'info',
loggingPretty: true,
poeContractAddress: '0x0e0989b1f9b8a38983c2ba8053269ca62ec9b195',
poeBalanceMinimum: 1000,
}
71 changes: 71 additions & 0 deletions src/controllers/ArchiveController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import * as Pino from 'pino'

import { PoetNode } from '../daos/PoetNodeDao'
import { PoeAddressNotVerified, PoeBalanceInsufficient } from '../errors/errors'
import { fetchBalance } from '../helpers/ethereum'
import { Network } from '../interfaces/Network'
import { Account } from '../models/Account'

export interface ArchiveController {
readonly postArchive: (
account: Account,
archive: ReadableStream,
network: Network,
) => Promise<any>
}

interface Arguments {
readonly dependencies: Dependencies
readonly configuration: Configuration
}

interface Dependencies {
readonly logger: Pino.Logger
readonly mainnetNode: PoetNode
readonly testnetNode: PoetNode
}

interface Configuration {
readonly poeContractAddress: string
readonly poeBalanceMinimum: number
}

export const ArchiveController = ({
dependencies: {
logger,
mainnetNode,
testnetNode,
},
configuration: {
poeContractAddress,
poeBalanceMinimum,
},
}: Arguments): ArchiveController => {
logger.info({ poeContractAddress, poeBalanceMinimum }, 'ArchiveController Instantiated')

const fetchPoeBalance = fetchBalance(poeContractAddress)

const networkToNode = (network: Network) => network === Network.LIVE ? mainnetNode : testnetNode

const postArchive = async (account: Account, archive: ReadableStream, network: Network) => {
const node = networkToNode(network)

const { id: userId, email, poeAddress, poeAddressVerified } = account

logger.debug({ userId, email, poeAddress, poeAddressVerified, network })

if (!poeAddressVerified)
throw new PoeAddressNotVerified()

const poeBalance = await fetchPoeBalance(poeAddress)

if (poeBalance < poeBalanceMinimum)
throw new PoeBalanceInsufficient(poeBalanceMinimum, poeBalance)

return node.postArchive(archive)
}

return {
postArchive,
}
}
27 changes: 27 additions & 0 deletions src/daos/PoetNodeDao.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as FormData from 'form-data'
import fetch from 'node-fetch'

export interface PoetNode {
readonly postArchive: (archive: any) => Promise<ReadonlyArray<{ readonly hash: string, readonly archiveUrl: string }>>
}

export const PoetNode = (url: string): PoetNode => {
const postArchive = async (archive: any) => {
const formData = new FormData()
formData.append('content', archive, { filepath: 'content' })

const response = await fetch(`${url}/files`, {
method: 'post',
body: formData,
})

if (!response.ok)
throw new Error(await response.text())

return response.json()
}

return {
postArchive,
}
}
20 changes: 18 additions & 2 deletions src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,11 @@ export class IncorrectOldPassword extends Error {
export class IncorrectToken extends Error {
status = 401

constructor(got: string, wanted: string) {
super(`Incorrect Token Type. Got "${got}", wanted "${wanted}".`)
constructor(got: string, wanted: string | string[]) {
super(
`Incorrect Token Type. Got "${got}", ` +
`wanted ${Array.isArray(wanted) ? `one of [${wanted.map(_ => `"${_}"`).join(', ')}]` : `"${wanted}"`}.`,
)
}
}

Expand All @@ -113,3 +116,16 @@ export class AuthenticationFailed extends Error {
status = errors.AuthenticationFailed.code
message = errors.AuthenticationFailed.message
}

export class PoeAddressNotVerified extends Error {
status = 403
message = 'POE address not verified.'
}

export class PoeBalanceInsufficient extends Error {
status = 403

constructor(minimum: number, balance: number) {
super(`Insufficient POE balance. You need at least ${minimum} POE. You currently have ${balance}.`)
}
}
5 changes: 5 additions & 0 deletions src/helpers/ethereum.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { bufferToHex, ecrecover, fromRpcSig, hashPersonalMessage, publicToAddress } from 'ethereumjs-util'
import fetch from 'node-fetch'

export function signatureIsValid(address: string, message: string, signature: string): boolean {
if (!address || !signature)
Expand Down Expand Up @@ -27,3 +28,7 @@ export function signatureIsValid(address: string, message: string, signature: st
throw exception
}
}

export const fetchBalance = (contractAddress: string) => (accountAddress: string) =>
fetch(`https://api.tokenbalance.com/balance/${contractAddress}/${accountAddress}`)
.then(_ => _.json())
2 changes: 1 addition & 1 deletion src/middlewares/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AccountController } from '../controllers/AccountController'

export const extractToken = (ctx: any): string => (ctx.header.token ? ctx.header.token : ctx.params.token) || ''

export const authorization = (accountController: AccountController) => async (ctx: any, next: any) => {
export const Authorization = (accountController: AccountController) => async (ctx: any, next: any) => {
// TODO: add configuration to ctx in app.ts so middlewares have access.
// This is needed until we can figure out how to restart vault between
// individual tests.
Expand Down

0 comments on commit f47d130

Please sign in to comment.