Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/router/signed_url_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
import type { Encryption } from '@boringnode/encryption'

import { type Router } from './main.ts'
import { createSignedURL } from '../helpers.ts'
import { type UrlFor, type LookupList, type SignedURLOptions } from '../types/url_builder.ts'
import { createSignedURL, parseRoute } from '../helpers.ts'
import { type LookupList, type SignedURLOptions, type SignedUrlFor } from '../types/url_builder.ts'

/**
* Creates the URLBuilder helper for making signed URLs
Expand All @@ -24,15 +24,34 @@ export function createSignedUrlBuilder<Routes extends LookupList>(
router: Router,
encryption: Encryption,
searchParamsStringifier: (qs: Record<string, any>) => string
): UrlFor<Routes, SignedURLOptions> {
): SignedUrlFor<Routes> {
let domainsList: string[]

function createSignedUrlForRoutePattern(
identifier: string,
params: any,
options?: SignedURLOptions
) {
return createSignedURL(
identifier,
parseRoute(identifier),
searchParamsStringifier,
encryption,
params,
options
)
}

function createSignedUrlForRoute(
identifier: string,
params: any,
options?: SignedURLOptions,
method?: string
) {
if (options?.disableRouteLookup) {
return createSignedUrlForRoutePattern(identifier, params, options)
}

if (!domainsList) {
domainsList = Object.keys(router.toJSON()).filter((domain) => domain !== 'root')
}
Expand All @@ -50,8 +69,10 @@ export function createSignedUrlBuilder<Routes extends LookupList>(
)
}

const signedRoute: UrlFor<Routes, SignedURLOptions> = function route(
...[identifier, params, options]
const signedRoute: SignedUrlFor<Routes> = function route(
identifier: string,
params?: any[] | Record<string, any>,
Comment thread
0xtlt marked this conversation as resolved.
Outdated
options?: SignedURLOptions
) {
return createSignedUrlForRoute(identifier, params, options)
}
Expand Down
19 changes: 19 additions & 0 deletions src/types/url_builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,27 @@ export type SignedURLOptions = URLOptions & {
expiresIn?: string | number
/** Purpose identifier for the signed URL */
purpose?: string
/** Make a signed URL using a route pattern without performing a route lookup */
disableRouteLookup?: boolean
}

/**
* Configuration options for signed URL generation without performing a route lookup
*/
export type SignedURLPatternOptions = SignedURLOptions & {
disableRouteLookup: true
}

/**
* URL builder helper for creating signed URLs.
*/
export type SignedUrlFor<Routes extends LookupList> = UrlFor<Routes, SignedURLOptions> &
((
identifier: string,
params: any[] | Record<string, any> | undefined,
options: SignedURLPatternOptions
) => string)

/**
* Utility type to extract routes for a specific HTTP method from the routes collection
*/
Expand Down
64 changes: 64 additions & 0 deletions tests/router/url_builder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { URLBuilderFactory } from '../../factories/url_builder_factory.ts'
import {
type UrlFor,
type URLOptions,
type SignedUrlFor,
type GetRoutesForMethod,
type RouteBuilderArguments,
} from '../../src/types/url_builder.ts'
Expand Down Expand Up @@ -290,6 +291,36 @@ test.group('URLBuilder', () => {
assert.isTrue(request.hasValidSignature())
})

test('create and verify signed URLs with route lookup disabled', async ({ assert }) => {
const encryption = new EncryptionFactory().create()
const { signedUrlFor } = new URLBuilderFactory().merge({ encryption }).create()

const signedUrl = signedUrlFor(
'/files/:key',
{ key: 'invoice.pdf' },
{
disableRouteLookup: true,
prefixUrl: 'http://localhost:4000',
qs: {
download: true,
},
}
)

const url = new URL(signedUrl)
const request = new HttpRequestFactory()
.merge({
encryption,
url: `${url.pathname}${url.search}`,
})
.create()

assert.equal(url.origin, 'http://localhost:4000')
assert.equal(url.pathname, '/files/invoice.pdf')
assert.equal(url.searchParams.get('download'), 'true')
assert.isTrue(request.hasValidSignature())
})

test('raise error when unable to lookup route', ({ assert }) => {
const router = new RouterFactory().create()
const { urlFor } = new URLBuilderFactory().merge({ router }).create()
Expand Down Expand Up @@ -684,4 +715,37 @@ test.group('URLBuilder | types', () => {
]
>()
})

test('accept route patterns when signed URL route lookup is disabled', ({ expectTypeOf }) => {
type Routes = {
ALL: {
'users.show': {
params: { id: string }
paramsTuple: [string]
}
}
GET: {
'users.show': {
params: { id: string }
paramsTuple: [string]
}
}
}

type RouteBuilder = SignedUrlFor<Routes>
const assertSignedUrlForTypes = (signedUrlFor: RouteBuilder) => {
expectTypeOf(
signedUrlFor(
'/files/:key',
{ key: 'invoice.pdf' },
{
disableRouteLookup: true,
prefixUrl: 'http://localhost:4000',
}
)
).toEqualTypeOf<string>()
}

expectTypeOf(assertSignedUrlForTypes).returns.toEqualTypeOf<void>()
})
})