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