refactor: lint formatting and fixes #6
@@ -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 { useEffect, useMemo, useState } from "react";
|
||||||
import { api } from "../api/apiClient";
|
import { api } from "../api/apiClient";
|
||||||
|
import type { LetterMetadata, LetterResponseData } from "../api/response";
|
||||||
import { endpoints } from "../config/endpoints";
|
import { endpoints } from "../config/endpoints";
|
||||||
import { useKeyStore } from "../store/useKeyStore";
|
import { useKeyStore } from "../store/useKeyStore";
|
||||||
import { CryptoUtils } from "../utils/crypto";
|
import { CryptoUtils } from "../utils/crypto";
|
||||||
|
|
||||||
export interface Letter {
|
export interface ProcessedLetter extends LetterResponseData {
|
||||||
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 {
|
|
||||||
metadata: LetterMetadata;
|
metadata: LetterMetadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decryptLettersMetadata(
|
async function decryptLettersMetadata(
|
||||||
letters: Letter[],
|
letters: LetterResponseData[],
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<ProcessedLetter[]> {
|
): Promise<ProcessedLetter[]> {
|
||||||
const cryptoUtils = new CryptoUtils();
|
const cryptoUtils = new CryptoUtils();
|
||||||
@@ -43,7 +27,7 @@ async function decryptLettersMetadata(
|
|||||||
)) as LetterMetadata;
|
)) as LetterMetadata;
|
||||||
|
|
||||||
return { ...letter, metadata };
|
return { ...letter, metadata };
|
||||||
} catch (_err) {
|
} catch {
|
||||||
return {
|
return {
|
||||||
...letter,
|
...letter,
|
||||||
metadata: { recipient: "Encrypted Letter" },
|
metadata: { recipient: "Encrypted Letter" },
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FlameIcon, PaperPlaneTiltIcon } from "@phosphor-icons/react";
|
import { FlameIcon, PaperPlaneTiltIcon } from "@phosphor-icons/react";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
type NavigateFunction,
|
type NavigateFunction,
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
useParams,
|
useParams,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { api } from "../api/apiClient";
|
import { api } from "../api/apiClient";
|
||||||
|
import type { LetterImageData, LetterResponseData } from "../api/response";
|
||||||
import {
|
import {
|
||||||
type CanvasJSON,
|
type CanvasJSON,
|
||||||
type CanvasTools,
|
type CanvasTools,
|
||||||
@@ -103,30 +105,54 @@ export default function Reader() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadAndDecrypt = async () => {
|
const decryptImages = async (
|
||||||
|
canvasData: CanvasJSON,
|
||||||
|
images: LetterImageData[],
|
||||||
|
encrypted_dek: string,
|
||||||
|
cryptoUtils: CryptoUtils,
|
||||||
|
) => {
|
||||||
|
if (!images?.length) return;
|
||||||
|
const isShared = !!sharingKey;
|
||||||
try {
|
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 {
|
const {
|
||||||
encrypted_content,
|
encrypted_content,
|
||||||
encrypted_metadata,
|
encrypted_metadata,
|
||||||
encrypted_dek,
|
encrypted_dek,
|
||||||
images,
|
images,
|
||||||
updated_at,
|
updated_at,
|
||||||
status,
|
} = data;
|
||||||
} = 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");
|
|
||||||
|
|
||||||
// Decrypt Metadata
|
// Decrypt Metadata
|
||||||
const decryptedMetadata = isShared
|
const decryptedMetadata = isShared
|
||||||
@@ -157,35 +183,29 @@ export default function Reader() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const canvasData: CanvasJSON = JSON.parse(decryptedContent);
|
const canvasData: CanvasJSON = JSON.parse(decryptedContent);
|
||||||
|
await decryptImages(canvasData, images, encrypted_dek, cryptoUtils);
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setDecryptedCanvasData(canvasData);
|
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) {
|
} catch (err) {
|
||||||
setLogTrace({
|
setLogTrace({
|
||||||
message: `Failed to load letter ☹`,
|
message: `Failed to load letter ☹`,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { LetterMetadata } from "../api/response";
|
||||||
|
|
||||||
export interface EncryptedLetter {
|
export interface EncryptedLetter {
|
||||||
encrypted_content: string;
|
encrypted_content: string;
|
||||||
encrypted_dek: string;
|
encrypted_dek: string;
|
||||||
@@ -275,7 +277,7 @@ export class CryptoUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async encryptMetadata(
|
public async encryptMetadata(
|
||||||
metadata: Record<string, any>,
|
metadata: LetterMetadata,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<EncryptedLetterMetadata> {
|
): Promise<EncryptedLetterMetadata> {
|
||||||
const { encryptedContent, encrypted_dek, sharingKey } =
|
const { encryptedContent, encrypted_dek, sharingKey } =
|
||||||
@@ -290,7 +292,7 @@ export class CryptoUtils {
|
|||||||
public async decryptMetadata(
|
public async decryptMetadata(
|
||||||
encrypted_metadata: EncryptedLetter,
|
encrypted_metadata: EncryptedLetter,
|
||||||
masterKey: CryptoKey,
|
masterKey: CryptoKey,
|
||||||
): Promise<Record<string, any>> {
|
): Promise<LetterMetadata> {
|
||||||
const bytes = await this.openEnvelope(
|
const bytes = await this.openEnvelope(
|
||||||
encrypted_metadata.encrypted_content,
|
encrypted_metadata.encrypted_content,
|
||||||
encrypted_metadata.encrypted_dek,
|
encrypted_metadata.encrypted_dek,
|
||||||
@@ -303,7 +305,7 @@ export class CryptoUtils {
|
|||||||
public async decryptMetadataWithSharingKey(
|
public async decryptMetadataWithSharingKey(
|
||||||
encrypted_content: string,
|
encrypted_content: string,
|
||||||
sharingKey: string,
|
sharingKey: string,
|
||||||
): Promise<Record<string, any>> {
|
): Promise<LetterMetadata> {
|
||||||
const bytes = await this.openEnvelopeWithSharingKey(
|
const bytes = await this.openEnvelopeWithSharingKey(
|
||||||
encrypted_content,
|
encrypted_content,
|
||||||
sharingKey,
|
sharingKey,
|
||||||
|
|||||||
@@ -221,7 +221,11 @@ describe("letterLogic image helpers", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
const remoteImages = [
|
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"]) });
|
vi.mocked(api.get).mockResolvedValue({ data: new Blob(["encrypted"]) });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { api, apiServerUrl, publicApi } from "../api/apiClient";
|
import { api, apiServerUrl, publicApi } from "../api/apiClient";
|
||||||
|
import type { LetterImageData } from "../api/response";
|
||||||
import type {
|
import type {
|
||||||
CanvasJSON,
|
CanvasJSON,
|
||||||
FabricImageJSON,
|
FabricImageJSON,
|
||||||
@@ -111,7 +112,7 @@ export async function decryptCanvasImages(
|
|||||||
|
|
||||||
export async function decryptCanvasImagesWithSharingKey(
|
export async function decryptCanvasImagesWithSharingKey(
|
||||||
canvasData: CanvasJSON,
|
canvasData: CanvasJSON,
|
||||||
remoteImages: { file_name: string; file: string }[],
|
remoteImages: LetterImageData[],
|
||||||
sharingKey: string,
|
sharingKey: string,
|
||||||
cryptoUtils: CryptoUtils,
|
cryptoUtils: CryptoUtils,
|
||||||
): Promise<DecryptionResult> {
|
): Promise<DecryptionResult> {
|
||||||
|
|||||||
Reference in New Issue
Block a user