From 0d8d6829909ab93e89e5fb87a67322283f5ff400 Mon Sep 17 00:00:00 2001 From: nobswebdev Date: Mon, 2 Nov 2026 14:20:12 +0100 Subject: [PATCH] Emit single-segment hotlink share and viewer URLs. Legacy /i and /h two-segment routes remain supported in the router and viewer. --- frontend/src/router/index.ts | 5 ++++ .../utils/share/buildHotlinkImagePath.spec.ts | 18 +++++++++---- .../src/utils/share/buildHotlinkImagePath.ts | 4 ++- .../share/buildHotlinkViewerPath.spec.ts | 15 +++++++++-- .../src/utils/share/buildHotlinkViewerPath.ts | 4 ++- .../share/buildUploadedShareUrls.spec.ts | 8 +++--- .../src/utils/share/buildUploadedShareUrls.ts | 4 +-- .../prepareHotlinkUploadRequest.spec.ts | 8 +++--- frontend/src/views/HotlinkViewerView.vue | 27 ++++++++++++++----- 9 files changed, 67 insertions(+), 26 deletions(-) diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 80a41ac..7db6924 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -35,6 +35,11 @@ const router = createRouter({ }, { path: '/h/:id/:secret', + component: () => import('@/views/HotlinkViewerView.vue'), + meta: { title: 'Private Image - Hotlink Viewer' } + }, + { + path: '/h/:accessToken', name: ROUTE_NAMES.HotlinkViewer, component: () => import('@/views/HotlinkViewerView.vue'), meta: { title: 'Private Image - Hotlink Viewer' } diff --git a/frontend/src/utils/share/buildHotlinkImagePath.spec.ts b/frontend/src/utils/share/buildHotlinkImagePath.spec.ts index 3f1eda5..d309d55 100644 --- a/frontend/src/utils/share/buildHotlinkImagePath.spec.ts +++ b/frontend/src/utils/share/buildHotlinkImagePath.spec.ts @@ -1,11 +1,19 @@ import { describe, expect, it } from 'vitest'; -import { buildHotlinkImagePath } from '@/utils/share/buildHotlinkImagePath'; +import { + buildHotlinkImagePath, + buildLegacyHotlinkImagePath +} from '@/utils/share/buildHotlinkImagePath'; describe('buildHotlinkImagePath', () => { + it('places the access token in a single path segment', () => { + expect(buildHotlinkImagePath('url-safe-access-token')).toBe('/i/url-safe-access-token'); + }); +}); + +describe('buildLegacyHotlinkImagePath', () => { it('places upload id and secret in the path segments', () => { - const uploadId = 'upload-uuid'; - const secret = 'url-safe-secret'; - - expect(buildHotlinkImagePath(uploadId, secret)).toBe(`/i/${uploadId}/${secret}`); + expect(buildLegacyHotlinkImagePath('upload-uuid', 'url-safe-secret')).toBe( + '/i/upload-uuid/url-safe-secret' + ); }); }); diff --git a/frontend/src/utils/share/buildHotlinkImagePath.ts b/frontend/src/utils/share/buildHotlinkImagePath.ts index 0c4e6ce..548ed44 100644 --- a/frontend/src/utils/share/buildHotlinkImagePath.ts +++ b/frontend/src/utils/share/buildHotlinkImagePath.ts @@ -1 +1,3 @@ -export const buildHotlinkImagePath = (uploadId: string, secret: string): string => `/i/${uploadId}/${secret}`; +export const buildHotlinkImagePath = (accessToken: string): string => `/i/${accessToken}`; + +export const buildLegacyHotlinkImagePath = (uploadId: string, secret: string): string => `/i/${uploadId}/${secret}`; diff --git a/frontend/src/utils/share/buildHotlinkViewerPath.spec.ts b/frontend/src/utils/share/buildHotlinkViewerPath.spec.ts index 110ee63..88e93c4 100644 --- a/frontend/src/utils/share/buildHotlinkViewerPath.spec.ts +++ b/frontend/src/utils/share/buildHotlinkViewerPath.spec.ts @@ -1,8 +1,19 @@ import { describe, expect, it } from 'vitest'; -import { buildHotlinkViewerPath } from '@/utils/share/buildHotlinkViewerPath'; +import { + buildHotlinkViewerPath, + buildLegacyHotlinkViewerPath +} from '@/utils/share/buildHotlinkViewerPath'; describe('buildHotlinkViewerPath', () => { + it('places the access token in a single viewer path segment', () => { + expect(buildHotlinkViewerPath('url-safe-access-token')).toBe('/h/url-safe-access-token'); + }); +}); + +describe('buildLegacyHotlinkViewerPath', () => { it('places upload id and secret in the viewer path', () => { - expect(buildHotlinkViewerPath('upload-uuid', 'url-safe-secret')).toBe('/h/upload-uuid/url-safe-secret'); + expect(buildLegacyHotlinkViewerPath('upload-uuid', 'url-safe-secret')).toBe( + '/h/upload-uuid/url-safe-secret' + ); }); }); diff --git a/frontend/src/utils/share/buildHotlinkViewerPath.ts b/frontend/src/utils/share/buildHotlinkViewerPath.ts index acb369c..38e069e 100644 --- a/frontend/src/utils/share/buildHotlinkViewerPath.ts +++ b/frontend/src/utils/share/buildHotlinkViewerPath.ts @@ -1 +1,3 @@ -export const buildHotlinkViewerPath = (uploadId: string, secret: string): string => `/h/${uploadId}/${secret}`; +export const buildHotlinkViewerPath = (accessToken: string): string => `/h/${accessToken}`; + +export const buildLegacyHotlinkViewerPath = (uploadId: string, secret: string): string => `/h/${uploadId}/${secret}`; diff --git a/frontend/src/utils/share/buildUploadedShareUrls.spec.ts b/frontend/src/utils/share/buildUploadedShareUrls.spec.ts index 12fd42a..f3b92ae 100644 --- a/frontend/src/utils/share/buildUploadedShareUrls.spec.ts +++ b/frontend/src/utils/share/buildUploadedShareUrls.spec.ts @@ -54,17 +54,17 @@ describe('buildUploadedShareUrls', () => { 'a & "shot"' ); - const directClearnet = `${clearnet}/i/upload-id/path_secret`; + const directClearnet = `${clearnet}/i/path_secret`; expect(urls).toEqual({ mode: 'hotlink', viewer: { - clearnet: `${clearnet}/h/upload-id/path_secret`, - onion: `${onion}/h/upload-id/path_secret` + clearnet: `${clearnet}/h/path_secret`, + onion: `${onion}/h/path_secret` }, direct: { clearnet: directClearnet, - onion: `${onion}/i/upload-id/path_secret` + onion: `${onion}/i/path_secret` }, delete: { clearnet: `${clearnet}/d/upload-id?token=a%2Bb%2Fc`, diff --git a/frontend/src/utils/share/buildUploadedShareUrls.ts b/frontend/src/utils/share/buildUploadedShareUrls.ts index 1112dd8..97325e8 100644 --- a/frontend/src/utils/share/buildUploadedShareUrls.ts +++ b/frontend/src/utils/share/buildUploadedShareUrls.ts @@ -30,8 +30,8 @@ export const buildUploadedShareUrls = ( }; if (upload.mode === 'hotlink') { - const viewerPath = buildHotlinkViewerPath(upload.uploadId, upload.urlSecret); - const directPath = buildHotlinkImagePath(upload.uploadId, upload.urlSecret); + const viewerPath = buildHotlinkViewerPath(upload.urlSecret); + const directPath = buildHotlinkImagePath(upload.urlSecret); const viewer = { clearnet: buildSiteShareUrl(clearnet, viewerPath), diff --git a/frontend/src/utils/upload/prepareHotlinkUploadRequest.spec.ts b/frontend/src/utils/upload/prepareHotlinkUploadRequest.spec.ts index f1c59ad..16296e2 100644 --- a/frontend/src/utils/upload/prepareHotlinkUploadRequest.spec.ts +++ b/frontend/src/utils/upload/prepareHotlinkUploadRequest.spec.ts @@ -64,17 +64,15 @@ describe('prepareHotlinkUploadRequest', () => { expect(recovered).toEqual(plaintext); }); - it('wires secret into the hotlink path segment', async () => { - const uploadId = 'hotlink-upload-id'; + it('wires secret into the hotlink direct path', async () => { const { request, secret } = await prepareHotlinkUploadRequest( new Uint8Array([1]), 'image/png', options ); - const path = buildHotlinkImagePath(uploadId, secret); + const path = buildHotlinkImagePath(secret); - expect(path).toBe(`/i/${uploadId}/${secret}`); + expect(path).toBe(`/i/${secret}`); expect(JSON.stringify(request)).not.toContain(secret); - expect(path.endsWith(`/${secret}`)).toBe(true); }); }); diff --git a/frontend/src/views/HotlinkViewerView.vue b/frontend/src/views/HotlinkViewerView.vue index 6fefeba..fea1ca8 100644 --- a/frontend/src/views/HotlinkViewerView.vue +++ b/frontend/src/views/HotlinkViewerView.vue @@ -28,7 +28,12 @@ import { onBeforeMount, ref } from 'vue'; import { useRoute } from 'vue-router'; import { config } from '@/config'; import type { HotlinkViewerViewState } from '@/types/view/HotlinkViewerViewState'; -import { buildHotlinkImagePath } from '@/utils/share/buildHotlinkImagePath'; +import { + buildHotlinkImagePath, + buildLegacyHotlinkImagePath +} from '@/utils/share/buildHotlinkImagePath'; + +const { baseUrlRoot } = config.api; const route = useRoute(); @@ -36,19 +41,29 @@ const state = ref('loading'); const imageSrc = ref(null); onBeforeMount(() => { + const accessToken = route.params.accessToken; const uploadId = route.params.id; const secret = route.params.secret; - if (typeof uploadId !== 'string' || typeof secret !== 'string' || !uploadId || !secret) { + let path: string | null = null; + + if (typeof accessToken === 'string' && accessToken) { + path = buildHotlinkImagePath(accessToken); + } else if ( + typeof uploadId === 'string' && + typeof secret === 'string' && + uploadId && + secret + ) { + path = buildLegacyHotlinkImagePath(uploadId, secret); + } + + if (!path) { state.value = 'invalidLink'; return; } - const { baseUrlRoot } = config.api; - - const path = buildHotlinkImagePath(uploadId, secret); - imageSrc.value = `${baseUrlRoot}${path}`; });