write delete endpoint
This commit is contained in:
parent
de5395bd23
commit
e6dc2d849a
2
plugin
2
plugin
@ -1 +1 @@
|
||||
Subproject commit 4ead2c609f30a2ad64b45bf9c19b4ba4e1de54e9
|
||||
Subproject commit 961634c1c45c1e3227617eac0700a9d0b1cf6417
|
73
server/src/controllers/note/note.delete.controller.ts
Normal file
73
server/src/controllers/note/note.delete.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
|
@ -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({
|
||||
|
21
server/src/lib/checkUserId.ts
Normal file
21
server/src/lib/checkUserId.ts
Normal 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;
|
||||
}
|
@ -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({
|
||||
|
40
server/src/validation/Request.ts
Normal file
40
server/src/validation/Request.ts
Normal 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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user