Support deleted notes filter for frontend

This commit is contained in:
Maxime Cannoodt 2022-11-21 22:19:32 +01:00
parent 855e0897f3
commit 017c318f88
9 changed files with 69 additions and 36 deletions

View File

@ -2,6 +2,7 @@ import { validateOrReject, ValidationError } from "class-validator";
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { deleteNote, getNote } from "../../db/note.dao"; import { deleteNote, getNote } from "../../db/note.dao";
import checkId from "../../lib/checkUserId"; import checkId from "../../lib/checkUserId";
import { getNoteFilter } from "../../lib/expiredNoteFilter";
import EventLogger, { WriteEvent } from "../../logging/EventLogger"; import EventLogger, { WriteEvent } from "../../logging/EventLogger";
import { getConnectingIp, getNoteSize } from "../../util"; import { getConnectingIp, getNoteSize } from "../../util";
import { NoteDeleteRequest } from "../../validation/Request"; import { NoteDeleteRequest } from "../../validation/Request";
@ -70,4 +71,7 @@ export async function deleteNoteController(
await EventLogger.deleteEvent(event); await EventLogger.deleteEvent(event);
next(err); next(err);
} }
const filter = await getNoteFilter("deletedNotes");
await filter.addNoteIds([note.id]);
} }

View File

@ -1,5 +1,5 @@
import { NextFunction, Request, Response } from "express"; import { NextFunction, Request, Response } from "express";
import { getExpiredNoteFilter } from "../../lib/expiredNoteFilter"; import { getNoteFilter } from "../../lib/expiredNoteFilter";
import EventLogger from "../../logging/EventLogger"; import EventLogger from "../../logging/EventLogger";
import { getConnectingIp, getNoteSize } from "../../util"; import { getConnectingIp, getNoteSize } from "../../util";
import { getNote } from "../../db/note.dao"; import { getNote } from "../../db/note.dao";
@ -21,8 +21,18 @@ export async function getNoteController(
res.send(note); res.send(note);
} else { } else {
// check the expired filter to see if the note was expired // check the expired filter to see if the note was expired
const expiredFilter = await getExpiredNoteFilter(); const deletedFilter = await getNoteFilter("deletedNotes");
if (expiredFilter.hasNoteId(req.params.id)) { 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({ await EventLogger.readEvent({
success: false, success: false,
host: ip, host: ip,
@ -37,7 +47,7 @@ export async function getNoteController(
note_id: req.params.id, note_id: req.params.id,
error: "Note not found", error: "Note not found",
}); });
res.status(404).send(); res.status(404).send("Note not found");
} }
} }
}) })

View File

@ -1,16 +1,23 @@
import { ScalableBloomFilter } from "bloom-filters"; import { ScalableBloomFilter } from "bloom-filters";
import { getFilter, upsertFilter } from "../db/bloomFilter.dao"; import { getFilter, upsertFilter } from "../db/bloomFilter.dao";
export class ExpiredNoteFilter { export const EXPIRED_NOTES_FILTER_NAME = "expiredNotes" as const;
_filter: ScalableBloomFilter; export const DELETED_NOTES_FILTER_NAME = "deletedNotes" as const;
static FILTER_NAME = "expiredNotes";
private constructor(filter: ScalableBloomFilter) { type FilterName =
| typeof EXPIRED_NOTES_FILTER_NAME
| typeof DELETED_NOTES_FILTER_NAME;
export class NoteIdFilter {
_filter: ScalableBloomFilter;
_name: string;
private constructor(name: string, filter: ScalableBloomFilter) {
this._filter = filter; this._filter = filter;
this._name = name;
} }
public static async deserializeFromDb(): Promise<ExpiredNoteFilter> { public static async deserializeFromDb(name: string): Promise<NoteIdFilter> {
return ExpiredNoteFilter._deserializeFilter() return NoteIdFilter._deserializeFilter(name)
.catch((err) => { .catch((err) => {
if (err.message === "No BloomFilter found") { if (err.message === "No BloomFilter found") {
return new ScalableBloomFilter(); return new ScalableBloomFilter();
@ -19,7 +26,7 @@ export class ExpiredNoteFilter {
} }
}) })
.then((filter) => { .then((filter) => {
return new ExpiredNoteFilter(filter); return new NoteIdFilter(name, filter);
}); });
} }
@ -35,24 +42,26 @@ export class ExpiredNoteFilter {
} }
private _serialize(): Promise<void> { private _serialize(): Promise<void> {
return upsertFilter(ExpiredNoteFilter.FILTER_NAME, this._filter); return upsertFilter(this._name, this._filter);
} }
private static _deserializeFilter(): Promise<ScalableBloomFilter> { private static _deserializeFilter(
return getFilter<ScalableBloomFilter>( name: string
this.FILTER_NAME, ): Promise<ScalableBloomFilter> {
ScalableBloomFilter return getFilter<ScalableBloomFilter>(name, ScalableBloomFilter);
);
} }
} }
let _filter: ExpiredNoteFilter; let _filters: Record<FilterName, NoteIdFilter | null> = {
expiredNotes: null,
deletedNotes: null,
};
export async function getExpiredNoteFilter(): Promise<ExpiredNoteFilter> { export async function getNoteFilter(name: FilterName): Promise<NoteIdFilter> {
if (_filter) { if (_filters[name] !== null) {
return _filter; return _filters[name] as NoteIdFilter;
} else { } else {
_filter = await ExpiredNoteFilter.deserializeFromDb(); _filters[name] = await NoteIdFilter.deserializeFromDb(name);
return _filter; return _filters[name] as NoteIdFilter;
} }
} }

View File

@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { ExpiredNoteFilter } from "./expiredNoteFilter"; import { NoteIdFilter } from "./expiredNoteFilter";
import { ScalableBloomFilter } from "bloom-filters"; import { ScalableBloomFilter } from "bloom-filters";
import * as dao from "../db/bloomFilter.dao"; import * as dao from "../db/bloomFilter.dao";
@ -16,12 +16,12 @@ describe("Deserialization from database", () => {
mockedDao.getFilter.mockRejectedValue(new Error("No BloomFilter found")); mockedDao.getFilter.mockRejectedValue(new Error("No BloomFilter found"));
// test instatiation // test instatiation
const testFilter = await ExpiredNoteFilter.deserializeFromDb(); const testFilter = await NoteIdFilter.deserializeFromDb();
expect(mockedDao.getFilter).toHaveBeenCalledWith( expect(mockedDao.getFilter).toHaveBeenCalledWith(
"expiredNotes", "expiredNotes",
ScalableBloomFilter ScalableBloomFilter
); );
expect(testFilter).toBeInstanceOf(ExpiredNoteFilter); expect(testFilter).toBeInstanceOf(NoteIdFilter);
// expect the _filter property to be a fresh ScalableBloomFilter (capacity 8) // expect the _filter property to be a fresh ScalableBloomFilter (capacity 8)
expect(testFilter._filter).toBeInstanceOf(ScalableBloomFilter); expect(testFilter._filter).toBeInstanceOf(ScalableBloomFilter);
@ -37,7 +37,7 @@ describe("Deserialization from database", () => {
mockedDao.getFilter.mockResolvedValue(bloomFilter); mockedDao.getFilter.mockResolvedValue(bloomFilter);
// test instatiation // test instatiation
const testFilter = await ExpiredNoteFilter.deserializeFromDb(); const testFilter = await NoteIdFilter.deserializeFromDb();
expect(mockedDao.getFilter).toHaveBeenCalledWith( expect(mockedDao.getFilter).toHaveBeenCalledWith(
"expiredNotes", "expiredNotes",
ScalableBloomFilter ScalableBloomFilter
@ -51,7 +51,7 @@ describe("Deserialization from database", () => {
}); });
describe("Filter operations and serialization", () => { describe("Filter operations and serialization", () => {
let testFilter: ExpiredNoteFilter; let testFilter: NoteIdFilter;
beforeEach(async () => { beforeEach(async () => {
const mockedDao = vi.mocked(dao); const mockedDao = vi.mocked(dao);
@ -63,7 +63,7 @@ describe("Filter operations and serialization", () => {
}); });
it("should add multiple noteIds to the filter", async () => { it("should add multiple noteIds to the filter", async () => {
testFilter = await ExpiredNoteFilter.deserializeFromDb(); testFilter = await NoteIdFilter.deserializeFromDb();
testFilter.addNoteIds(["test", "test2"]); testFilter.addNoteIds(["test", "test2"]);
expect(testFilter.hasNoteId("test")).toBe(true); expect(testFilter.hasNoteId("test")).toBe(true);
expect(testFilter.hasNoteId("test2")).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 () => { 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()); const elements = Array.from({ length: 1000 }, (_, i) => i.toString());
testFilter.addNoteIds(elements); testFilter.addNoteIds(elements);

View File

@ -1,5 +1,5 @@
import { deleteNotes, getExpiredNotes } from "../db/note.dao"; import { deleteNotes, getExpiredNotes } from "../db/note.dao";
import { getExpiredNoteFilter } from "../lib/expiredNoteFilter"; import { getNoteFilter } from "../lib/expiredNoteFilter";
import EventLogger from "../logging/EventLogger"; import EventLogger from "../logging/EventLogger";
import logger from "../logging/logger"; import logger from "../logging/logger";
import { getNoteSize } from "../util"; import { getNoteSize } from "../util";
@ -23,7 +23,7 @@ export async function deleteExpiredNotes(): Promise<number> {
}); });
}); });
await Promise.all(logs); await Promise.all(logs);
const filter = await getExpiredNoteFilter(); const filter = await getNoteFilter("expiredNotes");
await filter.addNoteIds(toDelete.map((n) => n.id)); await filter.addNoteIds(toDelete.map((n) => n.id));
logger.info(`[Cleanup] Deleted ${deleteCount} expired notes.`); logger.info(`[Cleanup] Deleted ${deleteCount} expired notes.`);
return deleteCount; return deleteCount;

View File

@ -38,7 +38,7 @@ describe("deleteExpiredNotes", () => {
mockedDao.deleteNotes.mockResolvedValue(1); mockedDao.deleteNotes.mockResolvedValue(1);
// mock ExpiredNoteFilter // mock ExpiredNoteFilter
const mockedFilter = vi.mocked(await filter.getExpiredNoteFilter()); const mockedFilter = vi.mocked(await filter.getNoteFilter());
mockedFilter.addNoteIds.mockResolvedValue(); mockedFilter.addNoteIds.mockResolvedValue();
// test task call // test task call

View File

@ -1,17 +1,22 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
console.log($page);
</script> </script>
<div class="prose max-w-2xl prose-zinc dark:prose-invert"> <div class="prose max-w-2xl prose-zinc dark:prose-invert">
{#if $page.status === 404} {#if $page.status === 404}
<h1>404: No note found 🕵️</h1> <h1>404: No note found 🕵️</h1>
<p class="prose-xl">No note was found at this link. Are you from the future?</p> <p class="prose-xl">No note was found at this link. Are you from the future?</p>
{:else if $page.status === 410} {:else if $page.status === 410 && $page.error?.message === 'Note expired'}
<h1>📝💨 This note is no longer here!</h1> <h1>📝💨 This note is no longer here!</h1>
<p class="prose-xl"> <p class="prose-xl">
Notes are stored for a limited amount of time. The note at this link was either set to expire, 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! or deleted due to inactivity. Sorry!
</p> </p>
{:else if $page.status === 410 && $page.error?.message === 'Note deleted'}
<h1>📝🗑 This note has been deleted.</h1>
<p class="prose-xl">The note at this link has been deleted by the user who shared it. Sorry!</p>
{:else} {:else}
<h1>Something went wrong 🤔</h1> <h1>Something went wrong 🤔</h1>
<p class="prose-xl"> <p class="prose-xl">
@ -24,8 +29,10 @@
{/if} {/if}
<div class="not-prose w-full flex justify-center mt-16"> <div class="not-prose w-full flex justify-center mt-16">
{#if $page.status === 404 || $page.status === 410} {#if $page.status === 404 || ($page.status === 410 && $page.error?.message === 'Note expired')}
<img src="/expired_note.svg" alt="encrypted-art" class="w-80" /> <img src="/expired_note.svg" alt="encrypted-art" class="w-80" />
{:else if $page.status === 410 && $page.error?.message === 'Note deleted'}
<img src="/deleted_note.svg" alt="encrypted-art" class="w-80" />
{/if} {/if}
</div> </div>
</div> </div>

View File

@ -28,6 +28,8 @@ export const load: PageServerLoad = async ({ request, params, setHeaders, getCli
throw error(500, response.statusText); throw error(500, response.statusText);
} }
} else { } 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);
} }
}; };

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB