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 { EncryptedNote } from "@prisma/client";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { crc16 as crc } from "crc";
|
|
||||||
import { createNote } from "../../db/note.dao";
|
import { createNote } from "../../db/note.dao";
|
||||||
import { addDays, getConnectingIp, getNoteSize } from "../../util";
|
import { addDays, getConnectingIp, getNoteSize } from "../../util";
|
||||||
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
|
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
|
||||||
import {
|
import { validateOrReject, ValidationError } from "class-validator";
|
||||||
validateOrReject,
|
|
||||||
IsBase64,
|
|
||||||
IsHexadecimal,
|
|
||||||
IsNotEmpty,
|
|
||||||
ValidateIf,
|
|
||||||
ValidationError,
|
|
||||||
Matches,
|
|
||||||
} from "class-validator";
|
|
||||||
import { generateToken } from "../../crypto/GenerateToken";
|
import { generateToken } from "../../crypto/GenerateToken";
|
||||||
|
import { NotePostRequest } from "../../validation/Request";
|
||||||
|
import checkId from "../../lib/checkUserId";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request body for creating a note
|
* 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(
|
export async function postNoteController(
|
||||||
req: Request,
|
req: Request,
|
||||||
@ -110,23 +79,3 @@ export async function postNoteController(
|
|||||||
next(err);
|
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 { vi, describe, it, beforeEach, afterEach, expect } from "vitest";
|
||||||
import * as noteDao from "../../db/note.dao";
|
import * as noteDao from "../../db/note.dao";
|
||||||
import EventLogger from "../../logging/EventLogger";
|
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("../../db/note.dao");
|
||||||
vi.mock("../../logging/EventLogger");
|
vi.mock("../../logging/EventLogger");
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import express from "express";
|
import express from "express";
|
||||||
import rateLimit from "express-rate-limit";
|
import rateLimit from "express-rate-limit";
|
||||||
|
import { deleteNoteController } from "./note.delete.controller";
|
||||||
import { getNoteController } from "./note.get.controller";
|
import { getNoteController } from "./note.get.controller";
|
||||||
import { postNoteController } from "./note.post.controller";
|
import { postNoteController } from "./note.post.controller";
|
||||||
|
|
||||||
@ -25,3 +26,4 @@ const getRateLimit = rateLimit({
|
|||||||
notesRoute.use(jsonParser);
|
notesRoute.use(jsonParser);
|
||||||
notesRoute.post("", postRateLimit, postNoteController);
|
notesRoute.post("", postRateLimit, postNoteController);
|
||||||
notesRoute.get("/:id", getRateLimit, getNoteController);
|
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> {
|
export async function deleteNotes(noteIds: string[]): Promise<number> {
|
||||||
return prisma.encryptedNote
|
return prisma.encryptedNote
|
||||||
.deleteMany({
|
.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 {
|
export enum EventType {
|
||||||
WRITE = "WRITE",
|
WRITE = "WRITE",
|
||||||
READ = "READ",
|
READ = "READ",
|
||||||
|
DELETE = "DELETE",
|
||||||
|
UPDATE = "UPDATE",
|
||||||
PURGE = "PURGE",
|
PURGE = "PURGE",
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Event {
|
export interface Event {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
@ -26,6 +28,10 @@ export interface WriteEvent extends ClientEvent {
|
|||||||
expire_window_days?: number;
|
expire_window_days?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DeleteEvent extends ClientEvent {}
|
||||||
|
|
||||||
|
interface UpdateEvent extends ClientEvent {}
|
||||||
|
|
||||||
interface ReadEvent extends ClientEvent {}
|
interface ReadEvent extends ClientEvent {}
|
||||||
|
|
||||||
interface PurgeEvent extends Event {
|
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> {
|
public static purgeEvent(event: PurgeEvent): Promise<event> {
|
||||||
this.printError(event);
|
this.printError(event);
|
||||||
return prisma.event.create({
|
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