write delete endpoint

This commit is contained in:
Maxime Cannoodt 2022-11-21 08:25:45 +01:00
parent de5395bd23
commit e6dc2d849a
10 changed files with 169 additions and 57 deletions

2
plugin

@ -1 +1 @@
Subproject commit 4ead2c609f30a2ad64b45bf9c19b4ba4e1de54e9
Subproject commit 961634c1c45c1e3227617eac0700a9d0b1cf6417

View File

@ -0,0 +1,73 @@
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 EventLogger, { WriteEvent } from "../../logging/EventLogger";
import { getConnectingIp, getNoteSize } from "../../util";
import { NoteDeleteRequest } from "../../validation/Request";
export async function deleteNoteController(
req: Request,
res: Response,
next: NextFunction
): Promise<void> {
const event: WriteEvent = {
success: false,
host: getConnectingIp(req),
user_id: req.body.user_id,
user_plugin_version: req.body.plugin_version,
};
// Validate request body
const noteDeleteRequest = new NoteDeleteRequest();
Object.assign(noteDeleteRequest, req.body);
try {
await validateOrReject(noteDeleteRequest);
} catch (_err: any) {
const err = _err as ValidationError;
res.status(400).send(err.toString());
event.error = err.toString();
await EventLogger.deleteEvent(event);
return;
}
// Validate user ID, if present
if (noteDeleteRequest.user_id && !checkId(noteDeleteRequest.user_id)) {
console.log("invalid user id");
res.status(400).send("Invalid user id (checksum failed)");
event.error = "Invalid user id (checksum failed)";
EventLogger.writeEvent(event);
return;
}
// get note from db
const note = await getNote(req.params.id);
if (!note) {
res.status(404).send("Note not found");
event.error = "Note not found";
await EventLogger.deleteEvent(event);
return;
}
// Validate secret token
if (note.secret_token !== req.body.secret_token) {
res.status(401).send("Invalid token");
event.error = "Invalid secret token";
await EventLogger.deleteEvent(event);
return;
}
// Delete note
try {
await deleteNote(note.id);
res.status(200);
event.success = true;
event.note_id = note.id;
event.size_bytes = getNoteSize(note);
await EventLogger.deleteEvent(event);
} catch (err) {
event.error = (err as Error).toString();
await EventLogger.deleteEvent(event);
next(err);
}
}

View File

@ -1,47 +1,16 @@
import { EncryptedNote } from "@prisma/client";
import { NextFunction, Request, Response } from "express";
import { crc16 as crc } from "crc";
import { createNote } from "../../db/note.dao";
import { addDays, getConnectingIp, getNoteSize } from "../../util";
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
import {
validateOrReject,
IsBase64,
IsHexadecimal,
IsNotEmpty,
ValidateIf,
ValidationError,
Matches,
} from "class-validator";
import { validateOrReject, ValidationError } from "class-validator";
import { generateToken } from "../../crypto/GenerateToken";
import { NotePostRequest } from "../../validation/Request";
import checkId from "../../lib/checkUserId";
/**
* Request body for creating a note
*/
export class NotePostRequest {
@IsBase64()
@IsNotEmpty()
ciphertext: string | undefined;
@IsBase64()
@ValidateIf((o) => !o.iv)
hmac?: string | undefined;
@IsBase64()
@ValidateIf((o) => !o.hmac)
iv?: string | undefined;
@ValidateIf((o) => o.user_id != null)
@IsHexadecimal()
user_id: string | undefined;
@ValidateIf((o) => o.plugin_version != null)
@Matches("^[0-9]+\\.[0-9]+\\.[0-9]+$")
plugin_version: string | undefined;
@Matches("^v[0-9]+$")
crypto_version: string = "v1";
}
export async function postNoteController(
req: Request,
@ -110,23 +79,3 @@ export async function postNoteController(
next(err);
});
}
/**
* @param id {string} a 16 character base16 string with 12 random characters and 4 CRC characters
* @returns {boolean} true if the id is valid, false otherwise
*/
function checkId(id: string): boolean {
// check length
if (id.length !== 16) {
return false;
}
// extract the random number and the checksum
const random = id.slice(0, 12);
const checksum = id.slice(12, 16);
// compute the CRC of the random number
const computedChecksum = crc(random).toString(16).padStart(4, "0");
// compare the computed checksum with the one in the id
return computedChecksum === checksum;
}

View File

@ -3,7 +3,8 @@ import supertest from "supertest";
import { vi, describe, it, beforeEach, afterEach, expect } from "vitest";
import * as noteDao from "../../db/note.dao";
import EventLogger from "../../logging/EventLogger";
import { NotePostRequest, postNoteController } from "./note.post.controller";
import { NotePostRequest } from "../../validation/Request";
import { postNoteController } from "./note.post.controller";
vi.mock("../../db/note.dao");
vi.mock("../../logging/EventLogger");

View File

@ -1,5 +1,6 @@
import express from "express";
import rateLimit from "express-rate-limit";
import { deleteNoteController } from "./note.delete.controller";
import { getNoteController } from "./note.get.controller";
import { postNoteController } from "./note.post.controller";
@ -25,3 +26,4 @@ const getRateLimit = rateLimit({
notesRoute.use(jsonParser);
notesRoute.post("", postRateLimit, postNoteController);
notesRoute.get("/:id", getRateLimit, getNoteController);
notesRoute.delete("/:id", getRateLimit, deleteNoteController);

View File

@ -23,6 +23,12 @@ export async function getExpiredNotes(): Promise<EncryptedNote[]> {
});
}
export async function deleteNote(noteId: string): Promise<EncryptedNote> {
return prisma.encryptedNote.delete({
where: { id: noteId },
});
}
export async function deleteNotes(noteIds: string[]): Promise<number> {
return prisma.encryptedNote
.deleteMany({

View File

@ -0,0 +1,21 @@
import { crc16 as crc } from "crc";
/**
* @param id {string} a 16 character base16 string with 12 random characters and 4 CRC characters
* @returns {boolean} true if the id is valid, false otherwise
*/
export default function checkId(id: string): boolean {
// check length
if (id.length !== 16) {
return false;
}
// extract the random number and the checksum
const random = id.slice(0, 12);
const checksum = id.slice(12, 16);
// compute the CRC of the random number
const computedChecksum = crc(random).toString(16).padStart(4, "0");
// compare the computed checksum with the one in the id
return computedChecksum === checksum;
}

View File

@ -5,10 +5,12 @@ import logger from "./logger";
export enum EventType {
WRITE = "WRITE",
READ = "READ",
DELETE = "DELETE",
UPDATE = "UPDATE",
PURGE = "PURGE",
}
interface Event {
export interface Event {
success: boolean;
error?: string;
}
@ -26,6 +28,10 @@ export interface WriteEvent extends ClientEvent {
expire_window_days?: number;
}
interface DeleteEvent extends ClientEvent {}
interface UpdateEvent extends ClientEvent {}
interface ReadEvent extends ClientEvent {}
interface PurgeEvent extends Event {
@ -54,6 +60,20 @@ export default class EventLogger {
});
}
public static deleteEvent(event: DeleteEvent): Promise<event> {
this.printError(event);
return prisma.event.create({
data: { type: EventType.DELETE, ...event },
});
}
public static updateEvent(event: UpdateEvent): Promise<event> {
this.printError(event);
return prisma.event.create({
data: { type: EventType.UPDATE, ...event },
});
}
public static purgeEvent(event: PurgeEvent): Promise<event> {
this.printError(event);
return prisma.event.create({

View File

@ -0,0 +1,40 @@
import {
IsBase64,
IsHexadecimal,
IsNotEmpty,
Matches,
ValidateIf,
} from "class-validator";
abstract class NoteRequestBody {
@ValidateIf((o) => o.user_id != null)
@IsHexadecimal()
user_id: string | undefined;
@ValidateIf((o) => o.plugin_version != null)
@Matches("^[0-9]+\\.[0-9]+\\.[0-9]+$")
plugin_version: string | undefined;
}
export class NotePostRequest extends NoteRequestBody {
@IsBase64()
@IsNotEmpty()
ciphertext: string | undefined;
@IsBase64()
@ValidateIf((o) => !o.iv)
hmac?: string | undefined;
@IsBase64()
@ValidateIf((o) => !o.hmac)
iv?: string | undefined;
@Matches("^v[0-9]+$")
crypto_version: string = "v1";
}
export class NoteDeleteRequest extends NoteRequestBody {
@IsBase64()
@IsNotEmpty()
secret_token: string | undefined;
}