Skip to content

Commit

Permalink
refactor: move DELETE /tokens/:id logic to controller (#965)
Browse files Browse the repository at this point in the history
  • Loading branch information
lautarodragan authored Jun 6, 2019
1 parent 0e56b58 commit b422da3
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 80 deletions.
77 changes: 4 additions & 73 deletions src/api/tokens/RemoveToken.ts
Original file line number Diff line number Diff line change
@@ -1,84 +1,15 @@
import * as Joi from 'joi'
import { verify } from 'jsonwebtoken'

import { AccountController } from '../../controllers/AccountController'
import { errors } from '../../errors/errors'
import { Account } from '../../models/Account'
import { Vault } from '../../utils/Vault/Vault'

const getApiTokens = (user: Account) => {
return user.apiTokens.map(({ token }) => token)
}

const getTestApiTokens = (user: Account) => {
return user.testApiTokens.map(({ token }) => token)
}

const decryptApiTokens = async (tokens: ReadonlyArray<string>) => {
const allTokens = tokens.map(Vault.decrypt, Vault)
return Promise.all(allTokens)
}

const encryptApiTokens = async (tokens: ReadonlyArray<string>) => {
const allTokens = tokens.map(Vault.encrypt, Vault)
return Promise.all(allTokens)
}

export const RemoveTokenSchema = () => ({
tokenId: Joi.string().required(),
})

export const RemoveToken = (accountController: AccountController) => async (ctx: any, next: any): Promise<any> => {
const logger = ctx.logger(__dirname)
const { ResourceNotFound } = errors

try {
const { user, jwtSecret } = ctx.state
const { tokenId } = ctx.params

const apiTokensDecrypted = await decryptApiTokens(getApiTokens(user))
const testApiTokensDecrypted = await decryptApiTokens(getTestApiTokens(user))

const apiTokensFiltered = apiTokensDecrypted.filter((token: string) => token !== tokenId)
const testApiTokensFiltered = testApiTokensDecrypted.filter((token: string) => token !== tokenId)

if (
apiTokensDecrypted.length === apiTokensFiltered.length &&
testApiTokensDecrypted.length === testApiTokensFiltered.length
) {
ctx.status = ResourceNotFound.code
ctx.body = ResourceNotFound.message
return
}

const { client_token } = verify(tokenId.replace('TEST_', ''), jwtSecret, {
ignoreExpiration: true,
}) as {
email: string
client_token: string
iat: number,
}

await Vault.revokeToken(client_token)
if (apiTokensDecrypted.length !== apiTokensFiltered.length) {
const apiTokensEncrypted = await encryptApiTokens(apiTokensFiltered)
const updateApiTokens = apiTokensEncrypted.map((token: string) => ({
token,
}))

await accountController.updateByIssuer(user.issuer, { apiTokens: updateApiTokens })
} else {
const testApiTokensEncrypted = await encryptApiTokens(testApiTokensFiltered)
const updateTestApiTokens = testApiTokensEncrypted.map((token: string) => ({
token,
}))

await accountController.updateByIssuer(user.issuer, { testApiTokens: updateTestApiTokens })
}
const { user } = ctx.state
const { tokenId } = ctx.params

ctx.status = 200
} catch (exception) {
logger.error(exception, 'api.RemoveToken')
ctx.status = 500
}
await accountController.removeToken(user, tokenId)
ctx.status = 200
}
50 changes: 45 additions & 5 deletions src/controllers/AccountController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,22 @@ import { getApiKeyByNetwork, getTokenByNetwork } from '../api/tokens/CreateToken
import { AccountDao } from '../daos/AccountDao'
import {
AccountAlreadyExists,
AccountNotFound, AuthenticationFailed, BadToken, EmailAlreadyVerified,
AccountNotFound,
AuthenticationFailed,
BadToken,
EmailAlreadyVerified,
IncorrectOldPassword,
IncorrectToken, InvalidToken,
IncorrectToken,
InvalidToken,
ResourceNotFound,
Unauthorized,
} from '../errors/errors'
import { tokenMatch } from '../helpers/token'
import { uuid4 } from '../helpers/uuid'
import { isJWTData, JWTData } from '../interfaces/JWTData'
import { Network } from '../interfaces/Network'
import { Account } from '../models/Account'
import { processPassword, passwordMatches } from '../utils/Password'
import { passwordMatches, processPassword } from '../utils/Password'
import { SendEmailTo } from '../utils/SendEmail'
import { Vault } from '../utils/Vault/Vault'

Expand All @@ -33,7 +38,7 @@ interface Authentication {
}

export interface AccountController {
readonly authorizeRequest: (token: string) => Promise<{ account: Account, tokenData: any, jwt: any }>
readonly authorizeRequest: (token: string) => Promise<{ account: Account, tokenData: any }>
readonly create: (e: EmailPassword) => Promise<Authentication>
readonly login: (e: EmailPassword) => Promise<Authentication>
readonly findByIssuer: (issuer: string) => Promise<Account>
Expand All @@ -55,6 +60,7 @@ export interface AccountController {
newPassword: string,
) => Promise<string>
readonly addToken: (issuer: string, email: string, network: Network) => Promise<string>
readonly removeToken: (user: Account, tokenId: string) => Promise<void>
readonly poeAddressChallenge: (issuer: string) => Promise<string>
}

Expand Down Expand Up @@ -88,7 +94,7 @@ export const AccountController = ({
const { client_token, email } = decodeJWT(token)
const tokenData = await Vault.verifyToken(client_token)
const account = await findByEmail(email)
return { jwt: configuration.jwtSecret, tokenData, account }
return { tokenData, account }
} catch (error) {
logger.error({ error }, 'Authorization Error')

Expand Down Expand Up @@ -265,6 +271,39 @@ export const AccountController = ({
return testOrMainApiToken
}

const removeToken = async (account: Account, token: string) => {
const decryptApiTokens = async (tokens: ReadonlyArray<string>) => {
const allTokens = tokens.map(Vault.decrypt, Vault)
return Promise.all(allTokens)
}

const encryptApiTokens = async (tokens: ReadonlyArray<string>) => {
const allTokens = tokens.map(Vault.encrypt, Vault)
return Promise.all(allTokens)
}

const { client_token, network } = decodeJWT(token)

const encryptedTokenObjects = network === Network.LIVE ? account.apiTokens : account.testApiTokens
const encryptedTokens = encryptedTokenObjects.map(({ token }) => token)
const tokens = await decryptApiTokens(encryptedTokens)

if (!tokens.find(_ => _ === token))
throw new ResourceNotFound()

await Vault.revokeToken(client_token)

const filteredTokens = tokens.filter((token: string) => token !== token)
const encryptedFilteredTokens = await encryptApiTokens(filteredTokens)
const encryptedFilteredTokensObjects = encryptedFilteredTokens.map(token => ({ token }))

const update = network === Network.LIVE
? { apiTokens: encryptedFilteredTokensObjects }
: { testApiTokens: encryptedFilteredTokensObjects }

await updateByIssuer(account.issuer, update)
}

const getToken = async (email: string, options: TokenOptions, network?: Network) => {
const tokenVault = await Vault.createToken(options)
const { client_token } = tokenVault.auth
Expand Down Expand Up @@ -303,6 +342,7 @@ export const AccountController = ({
changePassword,
changePasswordWithToken,
addToken,
removeToken,
poeAddressChallenge,
}
}
5 changes: 5 additions & 0 deletions src/errors/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export class AccountAlreadyExists extends Error {
message = errors.AccountAlreadyExists.message
}

export class ResourceNotFound {
status = 404
message = 'The specified resource does not exist.'
}

export class IncorrectOldPassword extends Error {
status = 403
message = 'Incorrect Old Password.'
Expand Down
3 changes: 1 addition & 2 deletions src/middlewares/authorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ export const Authorization = (accountController: AccountController) => async (ct

const token = extractToken(ctx)

const { account, tokenData, jwt } = await accountController.authorizeRequest(token)
const { account, tokenData } = await accountController.authorizeRequest(token)

if (!account) return (ctx.status = 404)

ctx.state.tokenData = tokenData
ctx.state.user = account
ctx.state.jwtSecret = jwt

return next()
}

0 comments on commit b422da3

Please sign in to comment.