feat(api): ✏️ add story api endpoints

(update/create/delete)
This commit is contained in:
parent ddf2fb9e3a
commit ff4b94d913
Signed by: tablet
GPG Key ID: 924A5F6AF051E87C
13 changed files with 359 additions and 0 deletions

@ -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;
}

@ -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;
}

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

@ -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;
}

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

@ -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";

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

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

@ -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,
};
});

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

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

@ -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(),
};
});

@ -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,
};
});