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