diff --git a/server/src/controllers/note/note.delete.controller.ts b/server/src/controllers/note/note.delete.controller.ts index a10eeec..04fb399 100644 --- a/server/src/controllers/note/note.delete.controller.ts +++ b/server/src/controllers/note/note.delete.controller.ts @@ -2,6 +2,7 @@ import { validateOrReject, ValidationError } from "class-validator"; import { NextFunction, Request, Response } from "express"; import { deleteNote, getNote } from "../../db/note.dao"; import checkId from "../../lib/checkUserId"; +import { getNoteFilter } from "../../lib/expiredNoteFilter"; import EventLogger, { WriteEvent } from "../../logging/EventLogger"; import { getConnectingIp, getNoteSize } from "../../util"; import { NoteDeleteRequest } from "../../validation/Request"; @@ -70,4 +71,7 @@ export async function deleteNoteController( await EventLogger.deleteEvent(event); next(err); } + + const filter = await getNoteFilter("deletedNotes"); + await filter.addNoteIds([note.id]); } diff --git a/server/src/controllers/note/note.get.controller.ts b/server/src/controllers/note/note.get.controller.ts index 0dad741..aca7089 100644 --- a/server/src/controllers/note/note.get.controller.ts +++ b/server/src/controllers/note/note.get.controller.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from "express"; -import { getExpiredNoteFilter } from "../../lib/expiredNoteFilter"; +import { getNoteFilter } from "../../lib/expiredNoteFilter"; import EventLogger from "../../logging/EventLogger"; import { getConnectingIp, getNoteSize } from "../../util"; import { getNote } from "../../db/note.dao"; @@ -21,8 +21,18 @@ export async function getNoteController( res.send(note); } else { // check the expired filter to see if the note was expired - const expiredFilter = await getExpiredNoteFilter(); - if (expiredFilter.hasNoteId(req.params.id)) { + const deletedFilter = await getNoteFilter("deletedNotes"); + const expiredFilter = await getNoteFilter("expiredNotes"); + + if (deletedFilter.hasNoteId(req.params.id)) { + await EventLogger.readEvent({ + success: false, + host: ip, + note_id: req.params.id, + error: "Note deleted", + }); + res.status(410).send("Note deleted"); + } else if (expiredFilter.hasNoteId(req.params.id)) { await EventLogger.readEvent({ success: false, host: ip, @@ -37,7 +47,7 @@ export async function getNoteController( note_id: req.params.id, error: "Note not found", }); - res.status(404).send(); + res.status(404).send("Note not found"); } } }) diff --git a/server/src/lib/expiredNoteFilter.ts b/server/src/lib/expiredNoteFilter.ts index 60c8466..722df2e 100644 --- a/server/src/lib/expiredNoteFilter.ts +++ b/server/src/lib/expiredNoteFilter.ts @@ -1,16 +1,23 @@ import { ScalableBloomFilter } from "bloom-filters"; import { getFilter, upsertFilter } from "../db/bloomFilter.dao"; -export class ExpiredNoteFilter { +export const EXPIRED_NOTES_FILTER_NAME = "expiredNotes" as const; +export const DELETED_NOTES_FILTER_NAME = "deletedNotes" as const; + +type FilterName = + | typeof EXPIRED_NOTES_FILTER_NAME + | typeof DELETED_NOTES_FILTER_NAME; +export class NoteIdFilter { _filter: ScalableBloomFilter; - static FILTER_NAME = "expiredNotes"; + _name: string; - private constructor(filter: ScalableBloomFilter) { + private constructor(name: string, filter: ScalableBloomFilter) { this._filter = filter; + this._name = name; } - public static async deserializeFromDb(): Promise { - return ExpiredNoteFilter._deserializeFilter() + public static async deserializeFromDb(name: string): Promise { + return NoteIdFilter._deserializeFilter(name) .catch((err) => { if (err.message === "No BloomFilter found") { return new ScalableBloomFilter(); @@ -19,7 +26,7 @@ export class ExpiredNoteFilter { } }) .then((filter) => { - return new ExpiredNoteFilter(filter); + return new NoteIdFilter(name, filter); }); } @@ -35,24 +42,26 @@ export class ExpiredNoteFilter { } private _serialize(): Promise { - return upsertFilter(ExpiredNoteFilter.FILTER_NAME, this._filter); + return upsertFilter(this._name, this._filter); } - private static _deserializeFilter(): Promise { - return getFilter( - this.FILTER_NAME, - ScalableBloomFilter - ); + private static _deserializeFilter( + name: string + ): Promise { + return getFilter(name, ScalableBloomFilter); } } -let _filter: ExpiredNoteFilter; +let _filters: Record = { + expiredNotes: null, + deletedNotes: null, +}; -export async function getExpiredNoteFilter(): Promise { - if (_filter) { - return _filter; +export async function getNoteFilter(name: FilterName): Promise { + if (_filters[name] !== null) { + return _filters[name] as NoteIdFilter; } else { - _filter = await ExpiredNoteFilter.deserializeFromDb(); - return _filter; + _filters[name] = await NoteIdFilter.deserializeFromDb(name); + return _filters[name] as NoteIdFilter; } } diff --git a/server/src/lib/expiredNoteFilter.unit.test.ts b/server/src/lib/expiredNoteFilter.unit.test.ts index f4b46eb..a71454b 100644 --- a/server/src/lib/expiredNoteFilter.unit.test.ts +++ b/server/src/lib/expiredNoteFilter.unit.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { ExpiredNoteFilter } from "./expiredNoteFilter"; +import { NoteIdFilter } from "./expiredNoteFilter"; import { ScalableBloomFilter } from "bloom-filters"; import * as dao from "../db/bloomFilter.dao"; @@ -16,12 +16,12 @@ describe("Deserialization from database", () => { mockedDao.getFilter.mockRejectedValue(new Error("No BloomFilter found")); // test instatiation - const testFilter = await ExpiredNoteFilter.deserializeFromDb(); + const testFilter = await NoteIdFilter.deserializeFromDb(); expect(mockedDao.getFilter).toHaveBeenCalledWith( "expiredNotes", ScalableBloomFilter ); - expect(testFilter).toBeInstanceOf(ExpiredNoteFilter); + expect(testFilter).toBeInstanceOf(NoteIdFilter); // expect the _filter property to be a fresh ScalableBloomFilter (capacity 8) expect(testFilter._filter).toBeInstanceOf(ScalableBloomFilter); @@ -37,7 +37,7 @@ describe("Deserialization from database", () => { mockedDao.getFilter.mockResolvedValue(bloomFilter); // test instatiation - const testFilter = await ExpiredNoteFilter.deserializeFromDb(); + const testFilter = await NoteIdFilter.deserializeFromDb(); expect(mockedDao.getFilter).toHaveBeenCalledWith( "expiredNotes", ScalableBloomFilter @@ -51,7 +51,7 @@ describe("Deserialization from database", () => { }); describe("Filter operations and serialization", () => { - let testFilter: ExpiredNoteFilter; + let testFilter: NoteIdFilter; beforeEach(async () => { const mockedDao = vi.mocked(dao); @@ -63,7 +63,7 @@ describe("Filter operations and serialization", () => { }); it("should add multiple noteIds to the filter", async () => { - testFilter = await ExpiredNoteFilter.deserializeFromDb(); + testFilter = await NoteIdFilter.deserializeFromDb(); testFilter.addNoteIds(["test", "test2"]); expect(testFilter.hasNoteId("test")).toBe(true); expect(testFilter.hasNoteId("test2")).toBe(true); @@ -77,7 +77,7 @@ describe("Filter operations and serialization", () => { }); it("Should have an error rate <1% for 1000 elements", async () => { - testFilter = await ExpiredNoteFilter.deserializeFromDb(); + testFilter = await NoteIdFilter.deserializeFromDb(); const elements = Array.from({ length: 1000 }, (_, i) => i.toString()); testFilter.addNoteIds(elements); diff --git a/server/src/tasks/deleteExpiredNotes.ts b/server/src/tasks/deleteExpiredNotes.ts index f185a2d..4342535 100644 --- a/server/src/tasks/deleteExpiredNotes.ts +++ b/server/src/tasks/deleteExpiredNotes.ts @@ -1,5 +1,5 @@ import { deleteNotes, getExpiredNotes } from "../db/note.dao"; -import { getExpiredNoteFilter } from "../lib/expiredNoteFilter"; +import { getNoteFilter } from "../lib/expiredNoteFilter"; import EventLogger from "../logging/EventLogger"; import logger from "../logging/logger"; import { getNoteSize } from "../util"; @@ -23,7 +23,7 @@ export async function deleteExpiredNotes(): Promise { }); }); await Promise.all(logs); - const filter = await getExpiredNoteFilter(); + const filter = await getNoteFilter("expiredNotes"); await filter.addNoteIds(toDelete.map((n) => n.id)); logger.info(`[Cleanup] Deleted ${deleteCount} expired notes.`); return deleteCount; diff --git a/server/src/tasks/deleteExpiredNotes.unit.test.ts b/server/src/tasks/deleteExpiredNotes.unit.test.ts index 3f6be4d..95c241e 100644 --- a/server/src/tasks/deleteExpiredNotes.unit.test.ts +++ b/server/src/tasks/deleteExpiredNotes.unit.test.ts @@ -38,7 +38,7 @@ describe("deleteExpiredNotes", () => { mockedDao.deleteNotes.mockResolvedValue(1); // mock ExpiredNoteFilter - const mockedFilter = vi.mocked(await filter.getExpiredNoteFilter()); + const mockedFilter = vi.mocked(await filter.getNoteFilter()); mockedFilter.addNoteIds.mockResolvedValue(); // test task call diff --git a/webapp/src/routes/+error.svelte b/webapp/src/routes/+error.svelte index 174a4b0..becd4d2 100644 --- a/webapp/src/routes/+error.svelte +++ b/webapp/src/routes/+error.svelte @@ -1,17 +1,22 @@
{#if $page.status === 404}

404: No note found 🕵️

No note was found at this link. Are you from the future?

- {:else if $page.status === 410} + {:else if $page.status === 410 && $page.error?.message === 'Note expired'}

📝💨 This note is no longer here!

Notes are stored for a limited amount of time. The note at this link was either set to expire, or deleted due to inactivity. Sorry!

+ {:else if $page.status === 410 && $page.error?.message === 'Note deleted'} +

📝🗑 This note has been deleted.

+

The note at this link has been deleted by the user who shared it. Sorry!

{:else}

Something went wrong 🤔

@@ -24,8 +29,10 @@ {/if}

- {#if $page.status === 404 || $page.status === 410} + {#if $page.status === 404 || ($page.status === 410 && $page.error?.message === 'Note expired')} encrypted-art + {:else if $page.status === 410 && $page.error?.message === 'Note deleted'} + encrypted-art {/if}
diff --git a/webapp/src/routes/note/[id]/+page.server.ts b/webapp/src/routes/note/[id]/+page.server.ts index a88de60..835d397 100644 --- a/webapp/src/routes/note/[id]/+page.server.ts +++ b/webapp/src/routes/note/[id]/+page.server.ts @@ -28,6 +28,8 @@ export const load: PageServerLoad = async ({ request, params, setHeaders, getCli throw error(500, response.statusText); } } else { - throw error(response.status, response.statusText); + // get the response body (the reason why the request failed) + const body = await response.text(); + throw error(response.status, body); } }; diff --git a/webapp/static/deleted_note.svg b/webapp/static/deleted_note.svg new file mode 100644 index 0000000..6b9cb88 --- /dev/null +++ b/webapp/static/deleted_note.svg @@ -0,0 +1 @@ + \ No newline at end of file