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…
Reference in New Issue
Block a user