Support deleted notes filter for frontend
This commit is contained in:
parent
855e0897f3
commit
017c318f88
@ -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]);
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
1
webapp/static/deleted_note.svg
Normal file
1
webapp/static/deleted_note.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 8.0 KiB |
Loading…
x
Reference in New Issue
Block a user