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 { 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]);
}

View File

@ -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");
}
}
})

View File

@ -1,16 +1,23 @@
import { ScalableBloomFilter } from "bloom-filters";
import { getFilter, upsertFilter } from "../db/bloomFilter.dao";
export class ExpiredNoteFilter {
_filter: ScalableBloomFilter;
static FILTER_NAME = "expiredNotes";
export const EXPIRED_NOTES_FILTER_NAME = "expiredNotes" as const;
export const DELETED_NOTES_FILTER_NAME = "deletedNotes" as const;
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._name = name;
}
public static async deserializeFromDb(): Promise<ExpiredNoteFilter> {
return ExpiredNoteFilter._deserializeFilter()
public static async deserializeFromDb(name: string): Promise<NoteIdFilter> {
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<void> {
return upsertFilter(ExpiredNoteFilter.FILTER_NAME, this._filter);
return upsertFilter(this._name, this._filter);
}
private static _deserializeFilter(): Promise<ScalableBloomFilter> {
return getFilter<ScalableBloomFilter>(
this.FILTER_NAME,
ScalableBloomFilter
);
private static _deserializeFilter(
name: string
): Promise<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> {
if (_filter) {
return _filter;
export async function getNoteFilter(name: FilterName): Promise<NoteIdFilter> {
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;
}
}

View File

@ -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);

View File

@ -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<number> {
});
});
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;

View File

@ -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

View File

@ -1,17 +1,22 @@
<script lang="ts">
import { page } from '$app/stores';
console.log($page);
</script>
<div class="prose max-w-2xl prose-zinc dark:prose-invert">
{#if $page.status === 404}
<h1>404: No note found 🕵️</h1>
<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>
<p class="prose-xl">
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!
</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}
<h1>Something went wrong 🤔</h1>
<p class="prose-xl">
@ -24,8 +29,10 @@
{/if}
<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" />
{:else if $page.status === 410 && $page.error?.message === 'Note deleted'}
<img src="/deleted_note.svg" alt="encrypted-art" class="w-80" />
{/if}
</div>
</div>

View File

@ -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);
}
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.0 KiB