Emit single-segment hotlink share and viewer URLs.
Legacy /i and /h two-segment routes remain supported in the router and viewer.
This commit is contained in:
@@ -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' }
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -54,17 +54,17 @@ describe('buildUploadedShareUrls', () => {
|
||||
'a <photo> & "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`,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<HotlinkViewerViewState>('loading');
|
||||
const imageSrc = ref<string | null>(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}`;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user