add ViewerView decrypt from URL fragment via view store
This commit is contained in:
@@ -0,0 +1,48 @@
|
|||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { api } from '@/plugins/axios';
|
||||||
|
import type { GetZkUploadCiphertextResult } from '@/types/upload/GetZkUploadCiphertextResult';
|
||||||
|
import { base64ToBytes } from '@/utils/encoding/base64ToBytes';
|
||||||
|
import { toArrayBuffer } from '@/utils/toArrayBuffer';
|
||||||
|
import { decryptZkUpload } from '@/utils/upload/decryptZkUpload';
|
||||||
|
|
||||||
|
export const useViewStore = defineStore('view', () => {
|
||||||
|
const imageObjectUrl = ref<string | null>(null);
|
||||||
|
|
||||||
|
const clearImage = (): void => {
|
||||||
|
if (imageObjectUrl.value) {
|
||||||
|
URL.revokeObjectURL(imageObjectUrl.value);
|
||||||
|
imageObjectUrl.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchZkCiphertext = async (uploadId: string): Promise<GetZkUploadCiphertextResult> => {
|
||||||
|
const { data } = await api.get<GetZkUploadCiphertextResult>(`/uploads/${uploadId}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadZk = async (payload: GetZkUploadCiphertextResult, fragmentKey: string): Promise<void> => {
|
||||||
|
clearImage();
|
||||||
|
|
||||||
|
const iv = base64ToBytes(payload.iv);
|
||||||
|
const ciphertext = base64ToBytes(payload.ciphertext);
|
||||||
|
|
||||||
|
const plaintext = await decryptZkUpload(ciphertext, iv, fragmentKey);
|
||||||
|
|
||||||
|
const buffer = toArrayBuffer(plaintext);
|
||||||
|
|
||||||
|
if (!buffer) {
|
||||||
|
throw new Error('Could not build image blob');
|
||||||
|
}
|
||||||
|
|
||||||
|
imageObjectUrl.value = URL.createObjectURL(new Blob([buffer]));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageObjectUrl,
|
||||||
|
fetchZkCiphertext,
|
||||||
|
loadZk,
|
||||||
|
clearImage
|
||||||
|
};
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export type GetZkUploadCiphertextResult = {
|
||||||
|
iv: string;
|
||||||
|
ciphertext: string;
|
||||||
|
passwordProtected: boolean;
|
||||||
|
wrappedDek?: string;
|
||||||
|
wrapIv?: string;
|
||||||
|
salt?: string;
|
||||||
|
};
|
||||||
@@ -1,6 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="flex min-h-screen flex-col p-8">
|
<main class="flex min-h-screen items-center justify-center p-4">
|
||||||
<h1 class="text-2xl font-semibold tracking-tight">Viewer</h1>
|
<p v-if="fetching" class="text-sm text-muted-foreground">Loading image...</p>
|
||||||
<p class="mt-2 text-muted-foreground">Zero-knowledge viewer — coming soon.</p>
|
|
||||||
|
<img v-else-if="imageObjectUrl" :src="imageObjectUrl" class="max-h-[80vh] max-w-full object-contain" />
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { onBeforeMount, onBeforeUnmount, ref } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { useViewStore } from '@/stores/view';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const viewStore = useViewStore();
|
||||||
|
const { imageObjectUrl } = storeToRefs(viewStore);
|
||||||
|
|
||||||
|
const fetching = ref(false);
|
||||||
|
|
||||||
|
onBeforeMount(async () => {
|
||||||
|
const uploadId = route.params.id;
|
||||||
|
|
||||||
|
if (typeof uploadId !== 'string') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fragmentKey = route.hash.slice(1);
|
||||||
|
|
||||||
|
if (!fragmentKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetching.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = await viewStore.fetchZkCiphertext(uploadId);
|
||||||
|
|
||||||
|
if (payload.passwordProtected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await viewStore.loadZk(payload, fragmentKey);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[viewer] load failed', { uploadId, error });
|
||||||
|
} finally {
|
||||||
|
fetching.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
viewStore.clearImage();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user