Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): manage incoming auth tokens #2935

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions localenv/mock-account-servicing-entity/generated/graphql.ts

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

24 changes: 24 additions & 0 deletions packages/backend/src/graphql/generated/graphql.schema.json

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

3 changes: 3 additions & 0 deletions packages/backend/src/graphql/generated/graphql.ts

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

3 changes: 3 additions & 0 deletions packages/backend/src/graphql/resolvers/peer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ export const peerToGraphql = (peer: Peer): SchemaPeer => ({
id: peer.id,
maxPacketAmount: peer.maxPacketAmount,
http: peer.http,
incomingTokens: peer.incomingTokens?.map(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think incomingTokens would probably benefit from a separate resolver kind of like liquidity is done. Check packages/backend/src/graphql/resolvers/index.ts. This will require a new function in the HttpTokenService instead of doing .withGraphFetched('incomingTokens') in the getter in peer service.

(incomingToken) => incomingToken.token
),
asset: assetToGraphql(peer.asset),
staticIlpAddress: peer.staticIlpAddress,
name: peer.name,
Expand Down
2 changes: 2 additions & 0 deletions packages/backend/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,8 @@ type Peer implements Model {
maxPacketAmount: UInt64
"Peering connection details"
http: Http!
"Incoming tokens"
incomingTokens: [String!]!
"Asset of peering relationship"
asset: Asset!
"Peer's ILP address"
Expand Down
23 changes: 14 additions & 9 deletions packages/backend/src/payment-method/ilp/peer/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,10 @@ async function getPeer(
deps: ServiceDependencies,
id: string
): Promise<Peer | undefined> {
return Peer.query(deps.knex).findById(id).withGraphFetched('asset')
return Peer.query(deps.knex)
.findById(id)
.withGraphFetched('asset')
.withGraphFetched('incomingTokens')
}

async function createPeer(
Expand Down Expand Up @@ -222,14 +225,16 @@ async function updatePeer(
return await Peer.transaction(deps.knex, async (trx) => {
if (options.http?.incoming) {
await deps.httpTokenService.deleteByPeer(options.id, trx)
const err = await addIncomingHttpTokens({
deps,
peerId: options.id,
tokens: options.http?.incoming?.authTokens,
trx
})
if (err) {
throw err
if (options.http?.incoming?.authTokens.length > 0) {
const err = await addIncomingHttpTokens({
deps,
peerId: options.id,
tokens: options.http?.incoming?.authTokens,
trx
})
if (err) {
throw err
}
}
}
return await Peer.query(trx)
Expand Down
149 changes: 149 additions & 0 deletions packages/frontend/app/components/ui/EditableTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import type { ReactNode } from 'react'
import { useEffect, useState } from 'react'
import { Input } from './Input'
import { Table } from './Table'
import { FieldError } from './FieldError'
import { Button } from './Button'

type EditableTableProps = {
name: string
label: string
options: EditableTableOption[]
error?: string | string[]
description?: ReactNode
required?: boolean
}

type EditableTableOption = {
label: string
value: string
canDelete?: boolean
canEdit?: boolean
showInput?: boolean
}

export const EditableTable = ({
name,
label,
options,
error,
description = undefined,
required = false
}: EditableTableProps) => {
const [optionsList, setOptionsList] = useState<EditableTableOption[]>(options)
const [values, setValues] = useState<string[]>()

const toggleEditInput = (index: number) => {
setOptionsList(
optionsList.map((option, i) => {
if (i === index) {
return {
...option,
showInput: true
}
}
return option
})
)
}

const editOption = (index: number, value: string) => {
if (!value) {
deleteOption(index)
return
}
setOptionsList(
optionsList.map((option, i) => {
if (i === index) {
return {
...option,
showInput: false,
value
}
}
return option
})
)
}

const deleteOption = (index: number) => {
setOptionsList(optionsList.filter((_, i) => i !== index))
}

const addOption = () => {
setOptionsList([
...optionsList,
{ label: '', value: '', canDelete: true, canEdit: true, showInput: true }
])
}

useEffect(
() => setValues(optionsList.map((option) => option.value)),
[optionsList]
)

return (
<>
<Input
type='hidden'
name={name}
value={values}
required={required}
label={label}
/>
<Table>
<Table.Head columns={['Token', 'Action']} />
<Table.Body>
{(optionsList || []).map((option, index) => (
<Table.Row key={index}>
<Table.Cell key={0}>
{option.showInput ? (
<Input
type='text'
onKeyDown={(e) =>
e.key === 'Enter' &&
(e.preventDefault(),
editOption(index, e.currentTarget.value))
}
defaultValue={option.value}
required={required}
/>
) : (
<span>{option.value}</span>
)}
</Table.Cell>
<Table.Cell key={1}>
{option.canEdit && !option.showInput ? (
<Button
aria-label='edit'
onClick={() => toggleEditInput(index)}
>
Edit
</Button>
) : null}
{option.canDelete ? (
<Button
className='ml-2'
aria-label='delete'
onClick={() => deleteOption(index)}
>
Delete
</Button>
) : null}
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
<div className='flex justify-end mt-2'>
<Button aria-label='add' onClick={() => addOption()}>
Add
</Button>
</div>
{description ? (
<div className='font-medium text-sm'>{description}</div>
) : null}
<FieldError error={error} />
</>
)
}
5 changes: 4 additions & 1 deletion packages/frontend/app/generated/graphql.ts

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

1 change: 1 addition & 0 deletions packages/frontend/app/lib/api/peer.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const getPeer = async (args: QueryPeerArgs) => {
authToken
}
}
incomingTokens
}
}
`,
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/app/lib/validate.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const peerGeneralInfoSchema = z

export const peerHttpInfoSchema = z
.object({
incomingAuthTokens: z.string().optional(),
incomingAuthTokens: z.array(z.string()),
outgoingAuthToken: z.string(),
outgoingEndpoint: z
.string()
Expand Down
22 changes: 15 additions & 7 deletions packages/frontend/app/routes/peers.$peerId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
import type { ZodFieldErrors } from '~/shared/types'
import { formatAmount } from '~/shared/utils'
import { checkAuthAndRedirect } from '../lib/kratos_checks.server'
import { EditableTable } from '~/components/ui/EditableTable'

export async function loader({ request, params }: LoaderFunctionArgs) {
const cookies = request.headers.get('cookie')
Expand Down Expand Up @@ -204,10 +205,15 @@ export default function ViewPeerPage() {
<fieldset disabled={currentPageAction}>
<div className='w-full p-4 space-y-3'>
<Input type='hidden' name='id' value={peer.id} />
<Input
<EditableTable
name='incomingAuthTokens'
label='Incoming Auth Tokens'
placeholder='Accepts a comma separated list of tokens'
options={(peer.incomingTokens || []).map((token) => ({
label: token,
value: token,
canDelete: true,
canEdit: true
}))}
error={response?.errors.http.fieldErrors.incomingAuthTokens}
description={
<>
Expand Down Expand Up @@ -428,23 +434,25 @@ export async function action({ request }: ActionFunctionArgs) {
break
}
case 'http': {
const result = peerHttpInfoSchema.safeParse(Object.fromEntries(formData))

const formDataEntries = Object.fromEntries(formData)
const result = peerHttpInfoSchema.safeParse({
...formDataEntries,
incomingAuthTokens: formDataEntries.incomingAuthTokens
? formDataEntries.incomingAuthTokens.toString().split(',')
: []
})
if (!result.success) {
actionResponse.errors.http.fieldErrors =
result.error.flatten().fieldErrors
return json({ ...actionResponse }, { status: 400 })
}

const response = await updatePeer({
id: result.data.id,
http: {
...(result.data.incomingAuthTokens
? {
incoming: {
authTokens: result.data.incomingAuthTokens
?.replace(/ /g, '')
.split(',')
}
}
: {}),
Expand Down
3 changes: 3 additions & 0 deletions packages/mock-account-service-lib/src/generated/graphql.ts

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

Loading