feat(api): ✏️ add story api endpoints
(update/create/delete)
This commit is contained in:
		
							parent
							
								
									ddf2fb9e3a
								
							
						
					
					
						commit
						ff4b94d913
					
				
							
								
								
									
										28
									
								
								lib/server/middlewareButNotReally/storyCheck.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								lib/server/middlewareButNotReally/storyCheck.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import type { H3Event, EventHandlerRequest } from "h3";
 | 
			
		||||
import type { Document } from "mongoose";
 | 
			
		||||
import { isFicmasHidden } from "~/lib/functions";
 | 
			
		||||
import { IStory } from "~/models/stories";
 | 
			
		||||
export default async function (
 | 
			
		||||
	event: H3Event<EventHandlerRequest>,
 | 
			
		||||
	story: IStory,
 | 
			
		||||
) {
 | 
			
		||||
	let ret: any = {};
 | 
			
		||||
	let num: number = event.context.chapterIndex;
 | 
			
		||||
	if (story.ficmas != null) {
 | 
			
		||||
		if (isFicmasHidden(story)) {
 | 
			
		||||
			ret = {
 | 
			
		||||
				statusCode: 423,
 | 
			
		||||
				message: `TOP SECRET! This story is part of an ongoing challenge. You'll be able to read it after the challenge's reveal date.`,
 | 
			
		||||
			};
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if (
 | 
			
		||||
		story.chapters[num]?.hidden ||
 | 
			
		||||
		(event.context.currentUser._id !== story.author._id &&
 | 
			
		||||
			!event.context.currentUser.isAdmin)
 | 
			
		||||
	) {
 | 
			
		||||
		ret.statusCode = 403;
 | 
			
		||||
		ret.message = "Forbidden";
 | 
			
		||||
	}
 | 
			
		||||
	return !!Object.keys(ret).length ? ret : null;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								lib/server/middlewareButNotReally/storyPrivileges.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								lib/server/middlewareButNotReally/storyPrivileges.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
import type { H3Event, EventHandlerRequest } from "h3";
 | 
			
		||||
import { IStory } from "~/models/stories";
 | 
			
		||||
export function canDelete(event: H3Event<EventHandlerRequest>, story: IStory) {
 | 
			
		||||
	return (
 | 
			
		||||
		event.context.currentUser?.profile.isAdmin ||
 | 
			
		||||
		story.author._id === event.context.currentUser?._id
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
export function canModify(event: H3Event<EventHandlerRequest>, story: IStory) {
 | 
			
		||||
	return event.context.currentUser?._id === story.author._id;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								lib/server/storyHelpers/bodyHandler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								lib/server/storyHelpers/bodyHandler.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
import { extname, resolve } from "path";
 | 
			
		||||
import { readFileSync } from "fs";
 | 
			
		||||
import { marked } from "marked";
 | 
			
		||||
import * as mammoth from "mammoth";
 | 
			
		||||
import * as san from "sanitize-html";
 | 
			
		||||
import { sanitizeConf } from "../constants";
 | 
			
		||||
import { FormChapter } from "~/lib/client/types/FormStory";
 | 
			
		||||
 | 
			
		||||
export default async function (bodyObj: FormChapter): Promise<string> {
 | 
			
		||||
	let str: string = "";
 | 
			
		||||
	if (bodyObj.content) {
 | 
			
		||||
		str = bodyObj.content;
 | 
			
		||||
	} else if (bodyObj.file) {
 | 
			
		||||
		let ext = extname(bodyObj.file).toLowerCase();
 | 
			
		||||
		if (ext === "md" || ext === "markdown")
 | 
			
		||||
			str = marked.parse(
 | 
			
		||||
				readFileSync(resolve(`tmp/${bodyObj.file}`)).toString(),
 | 
			
		||||
			);
 | 
			
		||||
		else if (ext === "doc" || ext === "docx")
 | 
			
		||||
			str = (
 | 
			
		||||
				await mammoth.convertToHtml(
 | 
			
		||||
					{ path: resolve(`tmp/${bodyObj.file}`) },
 | 
			
		||||
					{ styleMap: ["b => b", "i => i", "u => u"] },
 | 
			
		||||
				)
 | 
			
		||||
			).value;
 | 
			
		||||
		else
 | 
			
		||||
			throw createError({
 | 
			
		||||
				statusCode: 400,
 | 
			
		||||
				message: "bad file type",
 | 
			
		||||
			});
 | 
			
		||||
	} else {
 | 
			
		||||
		throw createError({
 | 
			
		||||
			statusCode: 400,
 | 
			
		||||
			message: "no content",
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	return san(str, sanitizeConf);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								lib/server/storyHelpers/formChapterTransform.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								lib/server/storyHelpers/formChapterTransform.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
import san from "sanitize-html";
 | 
			
		||||
import { FormChapter } from "~/lib/client/types/FormStory";
 | 
			
		||||
import { countWords } from "~/lib/functions";
 | 
			
		||||
import { IChapter } from "~/models/stories/chapter";
 | 
			
		||||
import { sanitizeConf } from "../constants";
 | 
			
		||||
import bodyHandler from "./bodyHandler";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default function(c: FormChapter): IChapter {
 | 
			
		||||
	let t: IChapter =  {
 | 
			
		||||
		title: c.chapterTitle,
 | 
			
		||||
		summary: san(c.summary, sanitizeConf),
 | 
			
		||||
		notes: san(c.notes, sanitizeConf),
 | 
			
		||||
		bands: c.bands,
 | 
			
		||||
		characters: c.characters,
 | 
			
		||||
		relationships: c.relationships,
 | 
			
		||||
		nsfw: c.nsfw,
 | 
			
		||||
		genre: c.genre,
 | 
			
		||||
		loggedInOnly: c.loggedInOnly,
 | 
			
		||||
		hidden: c.hidden
 | 
			
		||||
	}
 | 
			
		||||
	return t;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								lib/server/storyHelpers/getBucket.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								lib/server/storyHelpers/getBucket.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
import { GridFSBucket } from "mongodb";
 | 
			
		||||
import mongoose from "mongoose";
 | 
			
		||||
 | 
			
		||||
export default function () {
 | 
			
		||||
	// @ts-ignore SHUT UP MEG
 | 
			
		||||
	return new GridFSBucket(mongoose.connection.db, {
 | 
			
		||||
		bucketName: "story_text",
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										4
									
								
								lib/server/storyHelpers/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								lib/server/storyHelpers/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
			
		||||
export { default as bodyHandler } from "./bodyHandler";
 | 
			
		||||
export { default as getBucket } from "./getBucket";
 | 
			
		||||
export { default as replaceOrUploadContent } from "./replaceGridFS";
 | 
			
		||||
export { default as modelFormChapter } from "./formChapterTransform";
 | 
			
		||||
							
								
								
									
										16
									
								
								lib/server/storyHelpers/replaceGridFS.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								lib/server/storyHelpers/replaceGridFS.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
import getBucket from "./getBucket";
 | 
			
		||||
import {Readable} from "stream"
 | 
			
		||||
export default async function replaceGridFS(chapterID: number | undefined, content: string) {
 | 
			
		||||
	let filename = `/stories/${chapterID}.txt`;
 | 
			
		||||
	const bucket = getBucket()
 | 
			
		||||
	if(chapterID) {
 | 
			
		||||
		const curs = bucket.find({filename}).limit(1)
 | 
			
		||||
		for await(const d of curs) {
 | 
			
		||||
			await bucket.delete(d._id);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	const readable = new Readable();
 | 
			
		||||
	readable.push(content);
 | 
			
		||||
	readable.push(null);
 | 
			
		||||
	readable.pipe(bucket.openUploadStream(filename));
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								server/api/story/[id]/[chapter]/index.get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								server/api/story/[id]/[chapter]/index.get.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
import chapterTransformer from "~/lib/server/dbHelpers/chapterTransformer";
 | 
			
		||||
import storyQuerier from "~/lib/server/dbHelpers/storyQuerier";
 | 
			
		||||
import storyCheck from "~/lib/server/middlewareButNotReally/storyCheck";
 | 
			
		||||
 | 
			
		||||
export default eventHandler(async (ev) => {
 | 
			
		||||
	const story = await storyQuerier(ev);
 | 
			
		||||
	const chres = await storyCheck(ev, story);
 | 
			
		||||
	if (chres != null) {
 | 
			
		||||
		throw createError(chres);
 | 
			
		||||
	}
 | 
			
		||||
	return await chapterTransformer(story, ev);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										56
									
								
								server/api/story/[id]/[chapter]/index.put.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								server/api/story/[id]/[chapter]/index.put.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
			
		||||
import { FormChapter } from "~/lib/client/types/FormStory";
 | 
			
		||||
import { countWords } from "~/lib/functions";
 | 
			
		||||
import storyQuerier from "~/lib/server/dbHelpers/storyQuerier";
 | 
			
		||||
import isLoggedIn from "~/lib/server/middlewareButNotReally/isLoggedIn";
 | 
			
		||||
import { canModify } from "~/lib/server/middlewareButNotReally/storyPrivileges";
 | 
			
		||||
import { replaceContent, bodyHandler } from "~/lib/server/storyHelpers";
 | 
			
		||||
import { Story } from "~/models/stories";
 | 
			
		||||
 | 
			
		||||
export default eventHandler(async (ev) => {
 | 
			
		||||
	isLoggedIn(ev);
 | 
			
		||||
	const story = await storyQuerier(ev);
 | 
			
		||||
	if (!canModify(ev, story)) {
 | 
			
		||||
		throw createError({
 | 
			
		||||
			statusCode: 403,
 | 
			
		||||
			message: "Forbidden",
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	const body = await readBody(ev);
 | 
			
		||||
	const cc: FormChapter = body.chapters[0];
 | 
			
		||||
	const cid = story.chapters[ev.context.chapterIndex].id;
 | 
			
		||||
	const content = await bodyHandler(cc);
 | 
			
		||||
	await replaceContent(cid!, content);
 | 
			
		||||
	let ns;
 | 
			
		||||
	try {
 | 
			
		||||
		ns = await Story.findOneAndUpdate(
 | 
			
		||||
			{
 | 
			
		||||
				"chapters.id": cid,
 | 
			
		||||
			},
 | 
			
		||||
			{
 | 
			
		||||
				$set: {
 | 
			
		||||
					"chapters.$.title": cc.chapterTitle,
 | 
			
		||||
					"chapters.$.summary": cc.summary,
 | 
			
		||||
					"chapters.$.characters": cc.characters,
 | 
			
		||||
					"chapters.$.relationships": Array.from(new Set(cc.relationships)),
 | 
			
		||||
					"chapters.$.bands": cc.bands,
 | 
			
		||||
					"chapters.$.nsfw": !!cc.nsfw,
 | 
			
		||||
					"chapters.$.notes": cc.notes,
 | 
			
		||||
					"chapters.$.words": countWords(content),
 | 
			
		||||
					"chapters.$.genre": cc.genre,
 | 
			
		||||
					"chapters.$.loggedInOnly": !!cc.loggedInOnly,
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			{ new: true },
 | 
			
		||||
		);
 | 
			
		||||
	} catch (e: any) {
 | 
			
		||||
		throw createError({
 | 
			
		||||
			statusCode: 500,
 | 
			
		||||
			message: e.toString(),
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	return {
 | 
			
		||||
		data: ns,
 | 
			
		||||
		message: "Chapter updated",
 | 
			
		||||
		succes: true,
 | 
			
		||||
	};
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										18
									
								
								server/api/story/[id]/index.delete.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								server/api/story/[id]/index.delete.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
import storyQuerier from "~/lib/server/dbHelpers/storyQuerier";
 | 
			
		||||
import { canDelete } from "~/lib/server/middlewareButNotReally/storyPrivileges";
 | 
			
		||||
import { Story } from "~/models/stories";
 | 
			
		||||
 | 
			
		||||
export default eventHandler(async (ev) => {
 | 
			
		||||
	const tmpS = await storyQuerier(ev);
 | 
			
		||||
	if (canDelete(ev, tmpS)) {
 | 
			
		||||
		await Story.findByIdAndDelete(tmpS._id);
 | 
			
		||||
		return {
 | 
			
		||||
			success: true,
 | 
			
		||||
			message: "story deleted",
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
	throw createError({
 | 
			
		||||
		statusCode: 403,
 | 
			
		||||
		message: "Forbidden",
 | 
			
		||||
	});
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										15
									
								
								server/api/story/[id]/index.get.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								server/api/story/[id]/index.get.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import queryStory from "~/lib/server/dbHelpers/storyQuerier";
 | 
			
		||||
import storyCheck from "~/lib/server/middlewareButNotReally/storyCheck";
 | 
			
		||||
export default eventHandler(async (ev) => {
 | 
			
		||||
	const story = await queryStory(ev);
 | 
			
		||||
	let chrs = await storyCheck(ev, story);
 | 
			
		||||
	if (chrs != null) {
 | 
			
		||||
		throw createError(chrs);
 | 
			
		||||
	}
 | 
			
		||||
	if (story.chapters.some((a) => a.loggedInOnly) && !ev.context.currentUser)
 | 
			
		||||
		throw createError({
 | 
			
		||||
			statusCode: 401,
 | 
			
		||||
			message: "Authentication required",
 | 
			
		||||
		});
 | 
			
		||||
	return story;
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										80
									
								
								server/api/story/[id]/index.put.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								server/api/story/[id]/index.put.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,80 @@
 | 
			
		||||
import { Readable } from "stream";
 | 
			
		||||
import { Document } from "mongoose";
 | 
			
		||||
import { IStory, Story } from "~/models/stories";
 | 
			
		||||
import { FormStory } from "~/lib/client/types/FormStory";
 | 
			
		||||
import storyQuerier from "~/lib/server/dbHelpers/storyQuerier";
 | 
			
		||||
import isLoggedIn from "~/lib/server/middlewareButNotReally/isLoggedIn";
 | 
			
		||||
import { canModify } from "~/lib/server/middlewareButNotReally/storyPrivileges";
 | 
			
		||||
import {
 | 
			
		||||
	bodyHandler,
 | 
			
		||||
	getBucket,
 | 
			
		||||
	modelFormChapter,
 | 
			
		||||
	replaceOrUploadContent,
 | 
			
		||||
} from "~/lib/server/storyHelpers";
 | 
			
		||||
import { countWords } from "~/lib/functions";
 | 
			
		||||
 | 
			
		||||
export default eventHandler(async (ev) => {
 | 
			
		||||
	let os:
 | 
			
		||||
		| (Document<unknown, {}, IStory> &
 | 
			
		||||
				IStory &
 | 
			
		||||
				Required<{
 | 
			
		||||
					_id: number;
 | 
			
		||||
				}>)
 | 
			
		||||
		| null = await storyQuerier(ev);
 | 
			
		||||
	isLoggedIn(ev);
 | 
			
		||||
	if (!canModify(ev, os)) {
 | 
			
		||||
		throw createError({
 | 
			
		||||
			statusCode: 403,
 | 
			
		||||
			message: "Forbidden",
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	const body = await readBody<FormStory>(ev);
 | 
			
		||||
	const update: Partial<IStory> = {
 | 
			
		||||
		title: body.title,
 | 
			
		||||
		completed: body.completed,
 | 
			
		||||
		chapters: [],
 | 
			
		||||
	};
 | 
			
		||||
	for (const oc of os.chapters) {
 | 
			
		||||
		let filename = `/stories/${oc.id}.txt`;
 | 
			
		||||
		const bucket = getBucket();
 | 
			
		||||
		const curs = bucket.find({ filename }).limit(1);
 | 
			
		||||
		for await (const d of curs) {
 | 
			
		||||
			await bucket.delete(d._id);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for (const c of body.chapters) {
 | 
			
		||||
		let idx = os.chapters.findIndex((k) => k.id === c.id);
 | 
			
		||||
		const cont = await bodyHandler(c);
 | 
			
		||||
		if (idx === -1) {
 | 
			
		||||
			update.chapters!.push({
 | 
			
		||||
				...modelFormChapter(c),
 | 
			
		||||
				posted: new Date(Date.now()),
 | 
			
		||||
			});
 | 
			
		||||
		} else {
 | 
			
		||||
			update.chapters!.push({
 | 
			
		||||
				...modelFormChapter(c),
 | 
			
		||||
				id: os.chapters[idx].id,
 | 
			
		||||
				posted: os.chapters[idx].posted,
 | 
			
		||||
			});
 | 
			
		||||
			replaceOrUploadContent(os.chapters![idx].id, cont);
 | 
			
		||||
		}
 | 
			
		||||
		update.chapters![update.chapters!.length - 1].words = countWords(cont);
 | 
			
		||||
	}
 | 
			
		||||
	os = await Story.findOneAndUpdate(
 | 
			
		||||
		{
 | 
			
		||||
			_id: os._id,
 | 
			
		||||
		},
 | 
			
		||||
		update,
 | 
			
		||||
		{ new: true },
 | 
			
		||||
	);
 | 
			
		||||
	if (!os) {
 | 
			
		||||
		throw createError({
 | 
			
		||||
			statusCode: 500,
 | 
			
		||||
			message: "Something went wrong.",
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	return {
 | 
			
		||||
		success: true,
 | 
			
		||||
		data: os.toObject(),
 | 
			
		||||
	};
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										49
									
								
								server/api/story/new.post.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								server/api/story/new.post.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
import { Readable } from "stream";
 | 
			
		||||
import san from "sanitize-html";
 | 
			
		||||
import { FormStory } from "~/lib/client/types/FormStory";
 | 
			
		||||
import isLoggedIn from "~/lib/server/middlewareButNotReally/isLoggedIn";
 | 
			
		||||
import {
 | 
			
		||||
	getBucket,
 | 
			
		||||
	bodyHandler,
 | 
			
		||||
	modelFormChapter,
 | 
			
		||||
} from "~/lib/server/storyHelpers";
 | 
			
		||||
import { Story } from "~/models/stories";
 | 
			
		||||
import { sanitizeConf } from "~/lib/server/constants";
 | 
			
		||||
import { countWords } from "~/lib/functions";
 | 
			
		||||
 | 
			
		||||
export default eventHandler(async (ev) => {
 | 
			
		||||
	isLoggedIn(ev);
 | 
			
		||||
	const bucket = getBucket();
 | 
			
		||||
	const body = await readBody<FormStory>(ev);
 | 
			
		||||
	const story = new Story({
 | 
			
		||||
		title: body.title,
 | 
			
		||||
		author: ev.context.currentUser!._id,
 | 
			
		||||
		views: 0,
 | 
			
		||||
		reviews: 0,
 | 
			
		||||
		downloads: 0,
 | 
			
		||||
		ficmas: body.ficmas || null,
 | 
			
		||||
		challenge: body.challenge || null,
 | 
			
		||||
		completed: body.completed,
 | 
			
		||||
	});
 | 
			
		||||
	for (const c of body.chapters) {
 | 
			
		||||
		story.chapters.push(modelFormChapter(c));
 | 
			
		||||
		story.chapters[story.chapters.length - 1].words = countWords(
 | 
			
		||||
			await bodyHandler(c),
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
	await story.save();
 | 
			
		||||
 | 
			
		||||
	for (let i = 0; i < story.chapters.length; i++) {
 | 
			
		||||
		let c = story.chapters[i];
 | 
			
		||||
		const content = await bodyHandler(body.chapters[i]);
 | 
			
		||||
		const readable = new Readable();
 | 
			
		||||
		readable.push(content);
 | 
			
		||||
		readable.push(null);
 | 
			
		||||
		readable.pipe(bucket.openUploadStream(`/stories/${c.id}.txt`));
 | 
			
		||||
	}
 | 
			
		||||
	return {
 | 
			
		||||
		success: true,
 | 
			
		||||
		url: `/story/${story._id}/1`,
 | 
			
		||||
		story,
 | 
			
		||||
	};
 | 
			
		||||
});
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user