Skip to content

Commit d7b48fe

Browse files
rickyromboCopilotCopilot
authored
Add redirect_uri to dev app settings (#13909)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
1 parent 0b0433d commit d7b48fe

6 files changed

Lines changed: 129 additions & 31 deletions

File tree

packages/common/src/api/tan-query/developer-apps/useDeveloperApps.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,15 @@ export const useDeveloperApps = <TResult = DeveloperApp[]>(
3737
name,
3838
description,
3939
image_url,
40-
api_access_keys
40+
api_access_keys,
41+
redirect_uris
4142
}): DeveloperApp => ({
4243
name,
4344
description: description ?? undefined,
4445
imageUrl: image_url ?? undefined,
4546
apiKey: address.slice(2),
46-
api_access_keys
47+
api_access_keys,
48+
redirectUris: redirect_uris ?? undefined
4749
})
4850
)
4951
},

packages/common/src/api/tan-query/developer-apps/useEditDeveloperApp.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,21 @@ export const useEditDeveloperApp = () => {
1818
if (!currentUserId) {
1919
throw new Error('No current user ID')
2020
}
21-
const { name, description, imageUrl, apiKey } = editApp
21+
const { name, description, imageUrl, apiKey, redirectUris } = editApp
2222
const sdk = await audiusSdk()
2323

2424
await sdk.developerApps.updateDeveloperApp({
2525
address: apiKey,
2626
metadata: {
2727
name,
2828
description,
29-
imageUrl
29+
imageUrl,
30+
redirectUris
3031
},
3132
userId: Id.parse(currentUserId)
3233
})
3334

34-
return { name, description, imageUrl, apiKey }
35+
return { name, description, imageUrl, apiKey, redirectUris }
3536
},
3637
onSuccess: (editApp: DeveloperApp) => {
3738
queryClient.setQueryData(

packages/common/src/schemas/developerApps.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { z } from 'zod'
33
export const DEVELOPER_APP_DESCRIPTION_MAX_LENGTH = 128
44
export const DEVELOPER_APP_NAME_MAX_LENGTH = 50
55
export const DEVELOPER_APP_IMAGE_URL_MAX_LENGTH = 2000
6-
const DEVELOPER_APP_IMAGE_URL_REGEX = /^(https?):\/\//i
6+
const URL_REGEX = /^(https?):\/\//i
77

88
const messages = {
99
invalidUrl: 'Invalid URL'
@@ -23,6 +23,8 @@ export type DeveloperApp = {
2323
bearerToken?: string
2424
/** Bearer tokens from API when fetched with include=metrics */
2525
api_access_keys?: ApiAccessKey[]
26+
/** Pre-registered OAuth redirect/callback URIs */
27+
redirectUris?: string[]
2628
}
2729

2830
export const developerAppSchema = z.object({
@@ -31,7 +33,7 @@ export const developerAppSchema = z.object({
3133
z
3234
.string()
3335
.max(DEVELOPER_APP_IMAGE_URL_MAX_LENGTH)
34-
.refine((value) => DEVELOPER_APP_IMAGE_URL_REGEX.test(value), {
36+
.refine((value) => URL_REGEX.test(value), {
3537
message: messages.invalidUrl
3638
})
3739
),
@@ -45,11 +47,23 @@ export const developerAppEditSchema = z.object({
4547
z
4648
.string()
4749
.max(DEVELOPER_APP_IMAGE_URL_MAX_LENGTH)
48-
.refine((value) => DEVELOPER_APP_IMAGE_URL_REGEX.test(value), {
50+
.refine((value) => URL_REGEX.test(value), {
4951
message: messages.invalidUrl
5052
})
5153
),
52-
description: z.string().max(DEVELOPER_APP_DESCRIPTION_MAX_LENGTH).optional()
54+
description: z.string().max(DEVELOPER_APP_DESCRIPTION_MAX_LENGTH).optional(),
55+
redirectUris: z
56+
.array(
57+
z
58+
.string()
59+
.max(2000)
60+
.refine((value) => URL_REGEX.test(value), {
61+
message: messages.invalidUrl
62+
})
63+
.optional()
64+
)
65+
.max(50)
66+
.optional()
5367
})
5468

5569
export type NewAppPayload = Omit<DeveloperApp, 'apiKey'>

packages/sdk/src/sdk/api/developer-apps/DeveloperAppsApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export class DeveloperAppsApi extends GeneratedDeveloperAppsApi {
140140
description?: string | null
141141
image_url?: string | null
142142
api_access_keys?: Array<{ api_access_key: string; is_active: boolean }>
143+
redirect_uris?: string[]
143144
}>
144145
}
145146
return json

packages/sdk/src/sdk/api/developer-apps/types.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { isApiKeyValid } from '../../utils/apiKey'
66

77
const DEVELOPER_APP_MAX_DESCRIPTION_LENGTH = 128
88
const DEVELOPER_APP_MAX_IMAGE_URL_LENGTH = 2000
9-
const DEVELOPER_APP_IMAGE_URL_REGEX = /^(https?):\/\//i
9+
const URL_REGEX = /^(https?):\/\//i
1010

1111
export type DeveloperAppsApiServicesConfig = {
1212
entityManager?: EntityManagerService
@@ -19,12 +19,21 @@ export const CreateDeveloperAppSchema = z.object({
1919
z
2020
.string()
2121
.max(DEVELOPER_APP_MAX_IMAGE_URL_LENGTH)
22-
.refine((value) => DEVELOPER_APP_IMAGE_URL_REGEX.test(value), {
22+
.refine((value) => URL_REGEX.test(value), {
2323
message: 'Invalid URL'
2424
})
2525
),
2626
userId: HashId,
27-
redirectUris: z.array(z.string().max(2000)).max(50).optional()
27+
redirectUris: z
28+
.array(
29+
z
30+
.string()
31+
.max(2000)
32+
.refine((value) => URL_REGEX.test(value), {
33+
message: 'Invalid URL'
34+
})
35+
)
36+
.optional()
2837
})
2938

3039
export type EntityManagerCreateDeveloperAppRequest = z.input<
@@ -41,12 +50,21 @@ export const UpdateDeveloperAppSchema = z.object({
4150
z
4251
.string()
4352
.max(DEVELOPER_APP_MAX_IMAGE_URL_LENGTH)
44-
.refine((value) => DEVELOPER_APP_IMAGE_URL_REGEX.test(value), {
53+
.refine((value) => URL_REGEX.test(value), {
4554
message: 'Invalid URL'
4655
})
4756
),
4857
userId: HashId,
49-
redirectUris: z.array(z.string().max(2000)).max(50).optional()
58+
redirectUris: z
59+
.array(
60+
z
61+
.string()
62+
.max(2000)
63+
.refine((value) => URL_REGEX.test(value), {
64+
message: 'Invalid URL'
65+
})
66+
)
67+
.optional()
5068
})
5169

5270
export type EntityManagerUpdateDeveloperAppRequest = z.input<

packages/web/src/pages/settings-page/components/desktop/DeveloperApps/EditAppPage.tsx

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useCallback, useEffect, useState } from 'react'
1+
import { useCallback, useEffect, useMemo, useState } from 'react'
22

33
import {
44
DEVELOPER_APP_DESCRIPTION_MAX_LENGTH,
@@ -17,9 +17,11 @@ import {
1717
Button,
1818
Flex,
1919
IconEmbed,
20-
Divider
20+
Divider,
21+
IconPlus,
22+
Text
2123
} from '@audius/harmony'
22-
import { Form, Formik, useField } from 'formik'
24+
import { FieldArray, Form, Formik, useField } from 'formik'
2325
import { z } from 'zod'
2426
import { toFormikValidationSchema } from 'zod-formik-adapter'
2527

@@ -28,6 +30,7 @@ import { TextAreaField, TextField } from 'components/form-fields'
2830
import PreloadImage from 'components/preload-image/PreloadImage'
2931
import Toast from 'components/toast/Toast'
3032
import { copyToClipboard } from 'utils/clipboardUtil'
33+
import { removeNullable } from 'utils/typeUtils'
3134

3235
import styles from './EditAppPage.module.css'
3336
import { MaskedSecretDisplay } from './MaskedSecretDisplay'
@@ -53,7 +56,13 @@ const messages = {
5356
back: 'Back',
5457
save: 'Save Changes',
5558
saving: 'Saving',
56-
miscError: 'Sorry, something went wrong. Please try again later.'
59+
miscError: 'Sorry, something went wrong. Please try again later.',
60+
redirectUrisLabel: 'Registered Callback URLs',
61+
redirectUrisHelp:
62+
'Allowed values for the redirect_uri query parameter when using OAuth2 to obtain user access tokens.',
63+
removeRedirectUri: 'Remove redirect URI',
64+
addRedirectUri: 'Add Redirect URI',
65+
redirectUriPlaceholder: 'https://example.com/callback'
5766
}
5867

5968
const ImageField = ({ name }: { name: string }) => {
@@ -89,7 +98,7 @@ const getBearerTokens = (params: EditAppPageProps['params']) => {
8998

9099
export const EditAppPage = (props: EditAppPageProps) => {
91100
const { params, setPage } = props
92-
const { name, description, apiKey, imageUrl } = params ?? {}
101+
const { name, apiKey } = params ?? {}
93102
const initialBearerTokens = getBearerTokens(params)
94103
const [bearerTokens, setBearerTokens] =
95104
useState<string[]>(initialBearerTokens)
@@ -99,7 +108,6 @@ export const EditAppPage = (props: EditAppPageProps) => {
99108
const { isSuccess, isError, error, mutate, isPending } = useEditDeveloperApp()
100109
const deactivateAccessKey = useDeactivateDeveloperAppAccessKey()
101110
const createAccessKey = useCreateDeveloperAppAccessKey()
102-
const [submitError, setSubmitError] = useState<string | null>(null)
103111

104112
// Sync bearer tokens when params change (e.g. navigating to different app)
105113
useEffect(() => {
@@ -120,7 +128,6 @@ export const EditAppPage = (props: EditAppPageProps) => {
120128

121129
useEffect(() => {
122130
if (isError) {
123-
setSubmitError(messages.miscError)
124131
record(
125132
make(Name.DEVELOPER_APP_EDIT_ERROR, {
126133
error: error?.message
@@ -131,24 +138,33 @@ export const EditAppPage = (props: EditAppPageProps) => {
131138

132139
const handleSubmit = useCallback(
133140
(values: DeveloperAppValues) => {
134-
setSubmitError(null)
135141
record(
136142
make(Name.DEVELOPER_APP_EDIT_SUBMIT, {
137143
name: values.name,
138144
description: values.description
139145
})
140146
)
141-
mutate(values)
147+
// Trim redirect URIs and remove empty ones
148+
const redirectUris = (values.redirectUris ?? [])
149+
.map((u) => u?.trim())
150+
.filter(removeNullable)
151+
// Trim image URL and set to undefined if empty string
152+
const imageUrl = values.imageUrl?.trim() || undefined
153+
mutate({ ...values, redirectUris, imageUrl })
142154
},
143155
[mutate, record]
144156
)
145157

146-
const initialValues: DeveloperAppValues = {
147-
apiKey: apiKey || '',
148-
name: name || '',
149-
description,
150-
imageUrl
151-
}
158+
const initialValues: DeveloperAppValues = useMemo(
159+
() => ({
160+
apiKey: params?.apiKey || '',
161+
name: params?.name || '',
162+
description: params?.description,
163+
imageUrl: params?.imageUrl,
164+
redirectUris: params?.redirectUris?.length ? params.redirectUris : ['']
165+
}),
166+
[params]
167+
)
152168

153169
const copyApiKey = useCallback(() => {
154170
if (!apiKey) return
@@ -188,6 +204,7 @@ export const EditAppPage = (props: EditAppPageProps) => {
188204
initialValues={initialValues}
189205
onSubmit={handleSubmit}
190206
validationSchema={toFormikValidationSchema(developerAppEditSchema)}
207+
enableReinitialize
191208
>
192209
<Form>
193210
<Flex gap='m' direction='column'>
@@ -281,6 +298,51 @@ export const EditAppPage = (props: EditAppPageProps) => {
281298
>
282299
{messages.createNewToken}
283300
</Button>
301+
<Flex direction='column' gap='s'>
302+
<Text variant='body' strength='strong'>
303+
{messages.redirectUrisLabel}
304+
</Text>
305+
<Text variant='body' size='s' color='subdued'>
306+
{messages.redirectUrisHelp}
307+
</Text>
308+
<FieldArray name='redirectUris'>
309+
{({ push, remove, form }) => {
310+
const uris: string[] = form.values.redirectUris
311+
return (
312+
<>
313+
{uris.map((uri, index) => {
314+
const isLast = index === uris.length - 1
315+
return (
316+
<Flex key={index} gap='s' alignItems='center'>
317+
<TextField
318+
name={`redirectUris.${index}`}
319+
label={`${messages.addRedirectUri} ${index + 1}`}
320+
placeholder={messages.redirectUriPlaceholder}
321+
disabled={isPending}
322+
/>
323+
{isLast ? (
324+
<IconButton
325+
onClick={() => push('')}
326+
aria-label={messages.addRedirectUri}
327+
color='default'
328+
icon={IconPlus}
329+
/>
330+
) : (
331+
<IconButton
332+
onClick={() => remove(index)}
333+
aria-label={messages.removeRedirectUri}
334+
color='subdued'
335+
icon={IconTrash}
336+
/>
337+
)}
338+
</Flex>
339+
)
340+
})}
341+
</>
342+
)
343+
}}
344+
</FieldArray>
345+
</Flex>
284346
<div className={styles.actionsContainer}>
285347
<Button
286348
variant='secondary'
@@ -300,11 +362,11 @@ export const EditAppPage = (props: EditAppPageProps) => {
300362
{isPending ? messages.saving : messages.save}
301363
</Button>
302364
</div>
303-
{submitError == null ? null : (
365+
{isError ? (
304366
<div className={styles.errorContainer}>
305367
<span className={styles.errorText}>{messages.miscError}</span>
306368
</div>
307-
)}
369+
) : null}
308370
</Flex>
309371
</Form>
310372
</Formik>

0 commit comments

Comments
 (0)