refactor: add type interfaces for api data
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
export interface LetterResponseData {
|
||||
public_id: string;
|
||||
type: "KEPT" | "SENT" | "VAULT";
|
||||
status: "DRAFT" | "SEALED" | "BURNED";
|
||||
encrypted_content: string;
|
||||
encrypted_metadata: string;
|
||||
encrypted_dek: string;
|
||||
unlock_at: string | null;
|
||||
sealed_at: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
images: LetterImageData[];
|
||||
}
|
||||
|
||||
export interface LetterImageData {
|
||||
public_id: string;
|
||||
file: string;
|
||||
file_name: string;
|
||||
}
|
||||
|
||||
export interface LetterMetadata {
|
||||
recipient: string;
|
||||
tags?: string[];
|
||||
}
|
||||
@@ -1,32 +1,16 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { api } from "../api/apiClient";
|
||||
import type { LetterMetadata, LetterResponseData } from "../api/response";
|
||||
import { endpoints } from "../config/endpoints";
|
||||
import { useKeyStore } from "../store/useKeyStore";
|
||||
import { CryptoUtils } from "../utils/crypto";
|
||||
|
||||
export interface Letter {
|
||||
public_id: string;
|
||||
type: "KEPT" | "VAULT" | "SENT";
|
||||
status: "DRAFT" | "SEALED" | "BURNED";
|
||||
updated_at: string;
|
||||
sealed_at?: string;
|
||||
unlock_at: string;
|
||||
encrypted_metadata: string;
|
||||
encrypted_content: string;
|
||||
encrypted_dek: string;
|
||||
}
|
||||
|
||||
export interface LetterMetadata {
|
||||
recipient: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface ProcessedLetter extends Letter {
|
||||
export interface ProcessedLetter extends LetterResponseData {
|
||||
metadata: LetterMetadata;
|
||||
}
|
||||
|
||||
async function decryptLettersMetadata(
|
||||
letters: Letter[],
|
||||
letters: LetterResponseData[],
|
||||
masterKey: CryptoKey,
|
||||
): Promise<ProcessedLetter[]> {
|
||||
const cryptoUtils = new CryptoUtils();
|
||||
@@ -43,7 +27,7 @@ async function decryptLettersMetadata(
|
||||
)) as LetterMetadata;
|
||||
|
||||
return { ...letter, metadata };
|
||||
} catch (_err) {
|
||||
} catch {
|
||||
return {
|
||||
...letter,
|
||||
metadata: { recipient: "Encrypted Letter" },
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FlameIcon, PaperPlaneTiltIcon } from "@phosphor-icons/react";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
type NavigateFunction,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
useParams,
|
||||
} from "react-router-dom";
|
||||
import { api } from "../api/apiClient";
|
||||
import type { LetterImageData, LetterResponseData } from "../api/response";
|
||||
import {
|
||||
type CanvasJSON,
|
||||
type CanvasTools,
|
||||
@@ -103,30 +105,54 @@ export default function Reader() {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadAndDecrypt = async () => {
|
||||
const decryptImages = async (
|
||||
canvasData: CanvasJSON,
|
||||
images: LetterImageData[],
|
||||
encrypted_dek: string,
|
||||
cryptoUtils: CryptoUtils,
|
||||
) => {
|
||||
if (!images?.length) return;
|
||||
const isShared = !!sharingKey;
|
||||
try {
|
||||
const response = await api.get(`${endpoints.LETTERS}${public_id}/`);
|
||||
if (isShared) {
|
||||
await decryptCanvasImagesWithSharingKey(
|
||||
canvasData,
|
||||
images,
|
||||
sharingKey,
|
||||
cryptoUtils,
|
||||
);
|
||||
} else {
|
||||
await decryptCanvasImages(
|
||||
canvasData,
|
||||
images,
|
||||
encrypted_dek,
|
||||
// biome-ignore lint/style/noNonNullAssertion: masterKey is guaranteed to be non-null here as isDecryptionKeyAvailable is true
|
||||
masterKey!,
|
||||
cryptoUtils,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setLogTrace({
|
||||
message:
|
||||
"Failed to decrypt elements. Images might not render in the letter as intended.",
|
||||
log: err instanceof Error ? err.message : "Unknown error",
|
||||
type: "WARN",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const decryptLetterData = async (
|
||||
data: LetterResponseData,
|
||||
cryptoUtils: CryptoUtils,
|
||||
) => {
|
||||
const isShared = !!sharingKey;
|
||||
const {
|
||||
encrypted_content,
|
||||
encrypted_metadata,
|
||||
encrypted_dek,
|
||||
images,
|
||||
updated_at,
|
||||
status,
|
||||
} = response.data;
|
||||
|
||||
if (status === "BURNED")
|
||||
throw new Error("This letter has been burned.");
|
||||
|
||||
if (encrypted_dek) setEncryptedDek(encrypted_dek);
|
||||
|
||||
const cryptoUtils = new CryptoUtils();
|
||||
const isShared = !!sharingKey;
|
||||
|
||||
if (isShared && !encrypted_content) throw new Error("Content missing");
|
||||
const isDecryptionKeyAvailable = encrypted_dek && masterKey;
|
||||
if (!(isShared || isDecryptionKeyAvailable))
|
||||
throw new Error("Auth required: Decryption key is not available");
|
||||
} = data;
|
||||
|
||||
// Decrypt Metadata
|
||||
const decryptedMetadata = isShared
|
||||
@@ -157,35 +183,29 @@ export default function Reader() {
|
||||
);
|
||||
|
||||
const canvasData: CanvasJSON = JSON.parse(decryptedContent);
|
||||
|
||||
try {
|
||||
// Decrypt Images
|
||||
if (images?.length > 0) {
|
||||
isShared
|
||||
? await decryptCanvasImagesWithSharingKey(
|
||||
canvasData,
|
||||
images,
|
||||
sharingKey,
|
||||
cryptoUtils,
|
||||
)
|
||||
: await decryptCanvasImages(
|
||||
canvasData,
|
||||
images,
|
||||
encrypted_dek,
|
||||
// biome-ignore lint/style/noNonNullAssertion: masterKey is guaranteed to be non-null here as isDecryptionKeyAvailable is true
|
||||
masterKey!,
|
||||
cryptoUtils,
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setLogTrace({
|
||||
message:
|
||||
"Failed to decrypt elements. Images might not render in the letter as intended.",
|
||||
log: err instanceof Error ? err.message : "Unknown error",
|
||||
type: "WARN",
|
||||
});
|
||||
}
|
||||
await decryptImages(canvasData, images, encrypted_dek, cryptoUtils);
|
||||
setDecryptedCanvasData(canvasData);
|
||||
};
|
||||
|
||||
const loadAndDecrypt = async () => {
|
||||
try {
|
||||
const response: AxiosResponse<LetterResponseData> = await api.get(
|
||||
`${endpoints.LETTERS}${public_id}/`,
|
||||
);
|
||||
const data = response.data;
|
||||
|
||||
if (data.status === "BURNED")
|
||||
throw new Error("This letter has been burned.");
|
||||
|
||||
if (data.encrypted_dek) setEncryptedDek(data.encrypted_dek);
|
||||
|
||||
const isDecryptionKeyAvailable = data.encrypted_dek && masterKey;
|
||||
if (!(!!sharingKey || isDecryptionKeyAvailable)) {
|
||||
throw new Error("Auth required: Decryption key is not available");
|
||||
}
|
||||
|
||||
const cryptoUtils = new CryptoUtils();
|
||||
await decryptLetterData(data, cryptoUtils);
|
||||
} catch (err) {
|
||||
setLogTrace({
|
||||
message: `Failed to load letter ☹`,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { LetterMetadata } from "../api/response";
|
||||
|
||||
export interface EncryptedLetter {
|
||||
encrypted_content: string;
|
||||
encrypted_dek: string;
|
||||
@@ -275,7 +277,7 @@ export class CryptoUtils {
|
||||
}
|
||||
|
||||
public async encryptMetadata(
|
||||
metadata: Record<string, any>,
|
||||
metadata: LetterMetadata,
|
||||
masterKey: CryptoKey,
|
||||
): Promise<EncryptedLetterMetadata> {
|
||||
const { encryptedContent, encrypted_dek, sharingKey } =
|
||||
@@ -290,7 +292,7 @@ export class CryptoUtils {
|
||||
public async decryptMetadata(
|
||||
encrypted_metadata: EncryptedLetter,
|
||||
masterKey: CryptoKey,
|
||||
): Promise<Record<string, any>> {
|
||||
): Promise<LetterMetadata> {
|
||||
const bytes = await this.openEnvelope(
|
||||
encrypted_metadata.encrypted_content,
|
||||
encrypted_metadata.encrypted_dek,
|
||||
@@ -303,7 +305,7 @@ export class CryptoUtils {
|
||||
public async decryptMetadataWithSharingKey(
|
||||
encrypted_content: string,
|
||||
sharingKey: string,
|
||||
): Promise<Record<string, any>> {
|
||||
): Promise<LetterMetadata> {
|
||||
const bytes = await this.openEnvelopeWithSharingKey(
|
||||
encrypted_content,
|
||||
sharingKey,
|
||||
|
||||
@@ -221,7 +221,11 @@ describe("letterLogic image helpers", () => {
|
||||
],
|
||||
};
|
||||
const remoteImages = [
|
||||
{ file_name: "photo.png.bin", file: "https://remote/photo.png.bin" },
|
||||
{
|
||||
public_id: "1234",
|
||||
file_name: "photo.png.bin",
|
||||
file: "https://remote/photo.png.bin",
|
||||
},
|
||||
];
|
||||
|
||||
vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) });
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { api, apiServerUrl, publicApi } from "../api/apiClient";
|
||||
import type { LetterImageData } from "../api/response";
|
||||
import type {
|
||||
CanvasJSON,
|
||||
FabricImageJSON,
|
||||
@@ -111,7 +112,7 @@ export async function decryptCanvasImages(
|
||||
|
||||
export async function decryptCanvasImagesWithSharingKey(
|
||||
canvasData: CanvasJSON,
|
||||
remoteImages: { file_name: string; file: string }[],
|
||||
remoteImages: LetterImageData[],
|
||||
sharingKey: string,
|
||||
cryptoUtils: CryptoUtils,
|
||||
): Promise<DecryptionResult> {
|
||||
|
||||
Reference in New Issue
Block a user