Slave/shorten hotlink urls #15

Merged
nobswebdev merged 6 commits from slave/shorten-hotlink-urls into master 2026-11-02 14:18:07 +00:00
9 changed files with 67 additions and 26 deletions
Showing only changes of commit 0d8d682990 - Show all commits
+5
View File
@@ -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);
});
});
+21 -6
View File
@@ -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}`;
});