Skip to content
This repository has been archived by the owner on Mar 8, 2024. It is now read-only.

Commit

Permalink
feat: Admin API Matches Public API (#674)
Browse files Browse the repository at this point in the history
* feat: only accept one identity key in POST

* feat: use new version

* fix: version check

* refactor: reorder variables

* fixes

* fix: identity key count

* [2/x] Admin API Matches Public API: POST - Insert Verified User (#677)

* feat: new format POST insert

* refactor: names

* [3/x] Admin API Matches Public API: POST - Verify Identity Key + Insert Verified User ( Refactor ) (#678)

* refactor: parseVerified

* [4/x] Admin API Matches Public API : PUT - Verify Identity Key + Insert Verified User (#679)

* feat: PUT

* fix

* [5/x] Admin API Matches Public API : POST - Insert Un-verified User (#682)

* fix

* feat: parse all (#683)

* fix: drop status code

* lintfix

* New AdminAPI format POST tests (#676)

* feat: new format GET admin API

* POST tests for new AdminAPI format

* CLI -> canonical

* add identity key error catching tests

* fix: lint

* fix: remove GET tests

* fix: import versions from config instead of hardcoding

* fix tests

Co-authored-by: Austin King <[email protected]>
  • Loading branch information
dino-rodriguez and Austin King authored Aug 28, 2020
1 parent 6e47453 commit 663ef74
Show file tree
Hide file tree
Showing 6 changed files with 455 additions and 53 deletions.
4 changes: 3 additions & 1 deletion src/data-access/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,20 @@ export async function replaceUserPayId(
* @param oldPayId - The old PayID of the user.
* @param newPayId - The new PayID of the user.
* @param addresses - The array of payment address information to associate with this user.
* @param identityKey - The optional user identity key.
*
* @returns The updated payment addresses for a given PayID.
*/
export async function replaceUser(
oldPayId: string,
newPayId: string,
addresses: readonly AddressInformation[],
identityKey?: string,
): Promise<readonly AddressInformation[] | null> {
return knex.transaction(async (transaction: Transaction) => {
const updatedAddresses = await knex<Account>('account')
.where('payId', oldPayId)
.update({ payId: newPayId })
.update({ payId: newPayId, identityKey })
.transacting(transaction)
.returning('id')
.then(async (ids: ReadonlyArray<string | undefined>) => {
Expand Down
94 changes: 46 additions & 48 deletions src/middlewares/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
checkUserExistence,
} from '../data-access/users'
import { formatPaymentInfo } from '../services/basePayId'
import { AddressInformation } from '../types/database'
import parseAllAddresses from '../services/users'
import {
LookupError,
LookupErrorType,
Expand Down Expand Up @@ -119,32 +119,27 @@ export async function postUser(
ParseErrorType.MissingPayId,
)
}

const payId = rawPayId.toLowerCase()

// TODO:(hbergren) Need to test here and in `putUser()` that `req.body.addresses` is well formed.
// This includes making sure that everything that is not ACH or ILP is in a CryptoAddressDetails format.
// And that we `toUpperCase()` paymentNetwork and environment as part of parsing the addresses.
let allAddresses: AddressInformation[] = []

if (req.body.addresses !== undefined) {
allAddresses = allAddresses.concat(req.body.addresses)
}
if (req.body.verifiedAddresses !== undefined) {
allAddresses = allAddresses.concat(req.body.verifiedAddresses)
}
// * NOTE: We can be sure the version is defined because we verified it in checkRequestAdminApiVersionHeaders middleware
const [allAddresses, identityKey] = parseAllAddresses(
req.body.addresses,
req.body.verifiedAddresses,
req.body.identityKey,
String(req.get('PayID-API-Version')),
)

await insertUser(payId, allAddresses, req.body.identityKey)
await insertUser(payId, allAddresses, identityKey)

// Set HTTP status and save the PayID to generate the Location header in later middleware
res.locals.status = HttpStatus.Created
res.locals.payId = payId
next()
}

/* eslint-disable max-lines-per-function, max-statements, complexity --
* TODO: Remove all these disables when we refactor parsing/validation for the private API.
*/
/**
* Either create a new PayID, or update an existing PayID.
*
Expand All @@ -154,6 +149,7 @@ export async function postUser(
*
* @throws A ParseError if either PayID is missing or invalid.
*/
// eslint-disable-next-line max-lines-per-function -- Disabling until I finish building the functionality here.
export async function putUser(
req: Request,
res: Response,
Expand All @@ -163,7 +159,6 @@ export async function putUser(
// TODO(hbergren): pull this PayID / HttpError out into middleware?
const rawPayId = req.params.payId
const rawNewPayId = req.body?.payId
const identityKey = req.body?.identityKey

// TODO:(hbergren) More validation? Assert that the PayID is `$` and of a certain form?
// Do that using a regex route param in Express?
Expand Down Expand Up @@ -201,54 +196,57 @@ export async function putUser(
)
}

// We can be sure the version is defined because we verified it in checkRequestAdminApiVersionHeaders middleware
const requestVersion = String(req.get('PayID-API-Version'))
const payId = rawPayId.toLowerCase()
const newPayId = rawNewPayId.toLowerCase()

// TODO:(dino) validate body params before this
let allAddresses: AddressInformation[] = []
if (req.body.addresses !== undefined) {
allAddresses = allAddresses.concat(req.body.addresses)
}
if (req.body.verifiedAddresses !== undefined) {
allAddresses = allAddresses.concat(req.body.verifiedAddresses)
}

let updatedAddresses
let statusCode = HttpStatus.OK
const [allAddresses, identityKey] = parseAllAddresses(
req.body.addresses,
req.body.verifiedAddresses,
req.body.identityKey,
String(req.get('PayID-API-Version')),
)

updatedAddresses = await replaceUser(payId, newPayId, allAddresses)
// Attempt to replace user
let updatedAddresses = await replaceUser(
payId,
newPayId,
allAddresses,
identityKey,
)
// If user does not exist, create
if (updatedAddresses === null) {
updatedAddresses = await insertUser(newPayId, allAddresses, identityKey)
statusCode = HttpStatus.Created
}

// If the status code is 201 - Created, we need to set a Location header later with the PayID
if (statusCode === HttpStatus.Created) {
// If the status code is 201 - Created, we need to set a Location header later with the PayID
res.locals.status = HttpStatus.Created
res.locals.payId = newPayId
}

const addresses = updatedAddresses
.filter((address) => !address.identityKeySignature)
.map((address) => ({
paymentNetwork: address.paymentNetwork,
environment: address.environment,
details: address.details,
}))

const verifiedAddresses = updatedAddresses.filter((address) =>
Boolean(address.identityKeySignature),
)
// Only show created output on the "old" API
if (requestVersion < adminApiVersions[1]) {
const addresses = updatedAddresses
.filter((address) => !address.identityKeySignature)
.map((address) => ({
paymentNetwork: address.paymentNetwork,
environment: address.environment,
details: address.details,
}))

const verifiedAddresses = updatedAddresses.filter((address) =>
Boolean(address.identityKeySignature),
)

res.locals.status = statusCode
res.locals.response = {
payId: newPayId,
addresses,
verifiedAddresses,
res.locals.response = {
payId: newPayId,
addresses,
verifiedAddresses,
}
}

next()
}
/* eslint-enable max-lines-per-function, max-statements, complexity */

/**
* Removes a PayID from the PayID server.
Expand Down
147 changes: 147 additions & 0 deletions src/services/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { adminApiVersions } from '../config'
import { AddressInformation } from '../types/database'
import {
Address,
VerifiedAddress,
VerifiedAddressSignature,
} from '../types/protocol'
import { ParseError, ParseErrorType } from '../utils/errors'

/**
* Parse all addresses depending on the Admin API version and format them properly
* so they can be consumed by the database.
*
* @param maybeAddresses - An array of addresses ( in either the old or new format ) or undefined.
* @param maybeVerifiedAddresses - An array of verified addresses ( in either the old or new format ) or undefined.
* @param maybeIdentityKey - An identity key or undefined ( included with verified addresses ).
* @param requestVersion - The request version to determine how to parse the addresses.
*
* @returns A tuple of all the formatted addresses & the identity key.
*/
export default function parseAllAddresses(
maybeAddresses: Address[] | AddressInformation[] | undefined,
maybeVerifiedAddresses: VerifiedAddress[] | AddressInformation[] | undefined,
maybeIdentityKey: string | undefined,
requestVersion: string,
): [AddressInformation[], string | undefined] {
const addresses = maybeAddresses ?? []
const verifiedAddresses = maybeVerifiedAddresses ?? []
let allAddresses: AddressInformation[] = []

// If using "old" API format, we don't need to do any translation
if (requestVersion < adminApiVersions[1]) {
allAddresses = allAddresses.concat(
addresses as AddressInformation[],
verifiedAddresses as AddressInformation[],
)
}
// If using Public API format, we need to translate the payload so
// the data-access functions can consume them
else if (requestVersion >= adminApiVersions[1]) {
const formattedAddresses = (addresses as Address[]).map(
(address: Address) => {
return {
paymentNetwork: address.paymentNetwork,
...(address.environment && { environment: address.environment }),
details: address.addressDetails,
}
},
)
const formattedVerifiedAddressesAndKey = parseVerifiedAddresses(
verifiedAddresses as VerifiedAddress[],
)
allAddresses = allAddresses.concat(
formattedAddresses,
formattedVerifiedAddressesAndKey[0],
)
return [allAddresses, formattedVerifiedAddressesAndKey[1]]
}

return [allAddresses, maybeIdentityKey]
}

// HELPERS

/**
* Parse all verified addresses to confirm they use a single identity key &
* return parsed output that can be inserted into the database.
*
* @param verifiedAddresses - Array of verified addresses that adheres the the Public API format.
*
* @returns Array of address inforation to be consumed by insertUser.
*/
function parseVerifiedAddresses(
verifiedAddresses: VerifiedAddress[],
): [AddressInformation[], string | undefined] {
const identityKeyLabel = 'identityKey'
const formattedAddresses: AddressInformation[] = []
let identityKey: string | undefined

verifiedAddresses.forEach((verifiedAddress: VerifiedAddress) => {
let identityKeySignature: string | undefined
let identityKeyCount = 0

verifiedAddress.signatures.forEach(
(signaturePayload: VerifiedAddressSignature) => {
let decodedKey: { name: string }
try {
decodedKey = JSON.parse(
Buffer.from(signaturePayload.protected, 'base64').toString(),
)
} catch (_err) {
throw new ParseError(
'Invalid JSON for protected payload (identity key).',
ParseErrorType.InvalidIdentityKey,
)
}

// Get the first identity key & signature
if (!identityKey && decodedKey.name === identityKeyLabel) {
identityKey = signaturePayload.protected
identityKeyCount += 1
identityKeySignature = signaturePayload.signature
} else {
// Increment the count of identity keys per address
// And grab the signature for each address
if (decodedKey.name === identityKeyLabel) {
identityKeyCount += 1
identityKeySignature = signaturePayload.signature
}

// Identity key must match across all addresses
if (
identityKey !== signaturePayload.protected &&
decodedKey.name === identityKeyLabel
) {
throw new ParseError(
'More than one identity key detected. Only one identity key per PayID can be used.',
ParseErrorType.MultipleIdentityKeys,
)
}

// Each address must have only one identity key / signature pair
if (identityKeyCount > 1) {
throw new ParseError(
'More than one identity key detected. Only one identity key per address can be used.',
ParseErrorType.MultipleIdentityKeys,
)
}
}
},
)
// Transform to format consumable by insert user
// And add to all addresses
const jwsPayload = JSON.parse(verifiedAddress.payload)
const databaseAddressPayload = {
paymentNetwork: jwsPayload.payIdAddress.paymentNetwork,
environment: jwsPayload.payIdAddress.environment,
details: {
address: jwsPayload.payIdAddress.addressDetails.address,
},
identityKeySignature,
}
formattedAddresses.push(databaseAddressPayload)
})

return [formattedAddresses, identityKey]
}
6 changes: 3 additions & 3 deletions src/types/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,16 @@ export interface Address {
/**
* Object containing address information alongside signatures.
*/
interface VerifiedAddress {
export interface VerifiedAddress {
readonly payload: string
readonly signatures: readonly VerifiedAddressSignature[]
}

/**
* JWS object for verification.
*/
interface VerifiedAddressSignature {
name: string
export interface VerifiedAddressSignature {
name?: string
protected: string
signature: string
}
4 changes: 4 additions & 0 deletions src/utils/errors/parseError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export enum ParseErrorType {
MissingPayId = 'MissingPayId',
InvalidPayId = 'InvalidPayId',

// Verifiable PayID stuff
MultipleIdentityKeys = 'MultipleIdentityKeys',
InvalidIdentityKey = 'InvalidIdentityKey',

// These are the Public API version header errors for the PayID Protocol.
MissingPayIdVersionHeader = 'MissingPayIdVersionHeader',
InvalidPayIdVersionHeader = 'InvalidPayIdVersionHeader',
Expand Down
Loading

0 comments on commit 663ef74

Please sign in to comment.