Compare commits

..

10 Commits

17 changed files with 228 additions and 73 deletions

5
.gitignore vendored

@ -4,6 +4,7 @@
.nuxt
.nitro
.cache
.vite-nodez
dist
# Node dependencies
@ -32,10 +33,12 @@ stories/*.txt
# avatars
public/avatars/
!/**/.gitkeep
!/.idea/fileTemplates/
!/.idea/fileTemplates/**/*
!/.idea/conventionalcommit.json
!/.idea/conventionalCommit.xml
**/.auth
tests/screenshots/*.*
!/**/.gitkeep

@ -11,12 +11,26 @@
wrapCol?: any;
}>();
let dark = inject<boolean>("dark");
const stop = (e, ed) => {
console.log(e.type.toUpperCase(), e, ed);
e.preventDefault();
e.cancel();
e.stopImmediatePropagation();
e.stopPropagation();
};
</script>
<template>
<ClientOnly>
<vee-field :name="props.name" v-slot="{ errorMessage, field, value }" :model-value="props.val">
<a-form-item :validate-status="!!errorMessage ? 'error' : ''" :name="props.name" :label="props.label as any" :help="errorMessage">
<vee-field :name="props.name" v-slot="{ errorMessage, field, value, setValue }" :model-value="props.val">
<a-form-item
:data-testid="$attrs['data-testid']"
:validate-status="!!errorMessage ? 'error' : ''"
:name="props.name"
:label="props.label as any"
:help="errorMessage"
>
<tinymce-editor
model-events="drag dragdrop dragend draggesture dragover dragstart drop change"
v-bind="field"
width="100%"
@change="
@ -27,6 +41,16 @@
"
:initial-value="value"
v-model:model-value="field.value"
@drag="stop"
@deactivate="(e) => console.log(e)"
@dragOver="stop"
@dragEnter="stop"
@dragEnd="stop"
@dragDrop="stop"
@draggesture="stop"
@dragGesture="stop"
@dragstart="stop"
@drop="stop"
tinymce-script-src="/tinymce/tinymce.min.js"
:init="{
...props.init,

@ -67,6 +67,7 @@
:options="state.data"
v-model:value="value"
:allow-clear="true"
v-bind="$attrs"
>
<template v-if="state.fetching" #notFoundContent>
<a-spin size="small" />

@ -24,6 +24,7 @@
<template>
<a-form-item :validate-status="!!errorMessage ? 'error' : undefined" :help="errorMessage as any" label="Bands">
<a-select
v-bind="$attrs"
:allow-clear="true"
mode="multiple"
option-filter-prop="label"

@ -18,7 +18,7 @@
</script>
<template>
<a-form-item :help="errorMessage" label="Characters" :name="bandName as string" :validate-status="!!errorMessage ? 'error' : undefined">
<a-select mode="multiple" :options="opts" v-model:value="value">
<a-select v-bind="$attrs" mode="multiple" :options="opts" v-model:value="value">
<template #removeIcon>
<i class="far fa-circle-x" />
</template>

@ -10,7 +10,7 @@
</script>
<template>
<a-form-item :help="errorMessage" label="Genre(s)" :validate-status="!!errorMessage ? 'error' : undefined">
<a-select :allow-clear="true" :options="opts" v-model:value="value" mode="multiple">
<a-select v-bind="$attrs" :allow-clear="true" :options="opts" v-model:value="value" mode="multiple">
<template #removeIcon>
<i class="far fa-circle-x" />
</template>

@ -19,19 +19,19 @@
<a-form-item label="Pairings">
<a-row :gutter="5" :wrap="true" v-for="(field, idx) in fields" :key="field.key">
<Field :name="fname + 'relationships' + `[${idx}]`">
<a-select mode="multiple" :options="opts" v-model:value="field.value as string[]" @change="(val) => update(idx, val as string[])">
<a-select mode="multiple" v-bind="$attrs" :options="opts" v-model:value="field.value as string[]" @change="(val) => update(idx, val as string[])">
<template #removeIcon>
<i class="far fa-circle-x" />
</template>
</a-select>
</Field>
<a-col :span="4">
<a-button @click="(e) => remove(idx)"> - </a-button>
<a-button :data-testid="`${$attrs['data-testid']}.${idx}.remove`" @click="(e) => remove(idx)"> - </a-button>
</a-col>
</a-row>
<a-row justify="end">
<a-col :span="2">
<a-button @click="(e) => push([])"> + </a-button>
<a-button :data-testid="`${$attrs['data-testid']}.add`" @click="(e) => push([])"> + </a-button>
</a-col>
</a-row>
</a-form-item>

@ -9,6 +9,7 @@
import elCharacters from "../atoms/characters.vue";
import elPairings from "../atoms/pairings.vue";
import uploadOrPaste from "./uploadOrPaste.vue";
import { IBand } from "@models/band";
// import test1 from
let { name, data } = defineProps<{
@ -16,7 +17,7 @@
data: FormChapter;
}>();
let acData = toRef(data);
let { data: _bands } = await useApiFetch("/band/all");
let { data: _bands } = await useApiFetch<IBand[]>("/band/all");
let bands = ref(_bands);
provide("curName", name + ".");
provide("bandlist", bands);
@ -30,58 +31,59 @@
updateBands,
});
const wrapc = { span: 12 };
const tbase = `storyform.${name}.form`;
</script>
<template>
<div>
<div :data-testid="tbase">
<a-row :gutter="[10, 0]">
<a-col :span="12">
<Field :name="name + '.chapterTitle'" v-slot="{ value, field, errorMessage }">
<a-form-item :name="[field.name as string]" label="Chapter title" :help="errorMessage" :status="!!errorMessage ? 'error' : undefined">
<a-input v-bind="field" />
<a-input :data-testid="`${tbase}.title`" v-bind="field" />
</a-form-item>
</Field>
</a-col>
<a-col :span="12">
<el-bands />
<el-bands :data-testid="`${tbase}.bands`" />
</a-col>
</a-row>
<a-row :gutter="[10, 0]">
<a-col :span="12">
<base-editor v-model:val="acData.summary" :name="name + '.summary'" :wrap-col="wrapc" label="Summary" :init="bare" />
<base-editor :data-testid="`${tbase}.summary`" v-model:val="acData.summary" :name="name + '.summary'" :wrap-col="wrapc" label="Summary" :init="bare" />
</a-col>
<a-col :span="12">
<base-editor v-model:val="acData.notes" :name="name + '.notes'" :wrap-col="wrapc" label="Author's notes" :init="bare" />
<base-editor :data-testid="`${tbase}.notes`" v-model:val="acData.notes" :name="name + '.notes'" :wrap-col="wrapc" label="Author's notes" :init="bare" />
</a-col>
</a-row>
<a-row :gutter="[10, 0]">
<a-col :span="12">
<el-characters />
<el-characters :data-testid="`${tbase}.characters`" />
</a-col>
<a-col :span="12">
<el-pairings />
<el-pairings :data-testid="`${tbase}.relationships`" />
</a-col>
</a-row>
<Field :name="name + '.nsfw'" type="checkbox" :unchecked-value="false" :value="true" v-slot="{ value, field, errorMessage }">
<a-checkbox v-bind="field" v-model="field.value"> Has NSFW content </a-checkbox>
<a-checkbox :data-testid="`${tbase}.nsfw`" v-bind="field" v-model="field.value"> Has NSFW content </a-checkbox>
<error-message :name="field.name" />
</Field>
<Field :name="name + '.loggedInOnly'" type="checkbox" :unchecked-value="false" :value="true" v-slot="{ value, field, errorMessage }">
<a-checkbox v-bind="field"> Visible only to registered users </a-checkbox>
<a-checkbox :data-testid="`${tbase}.loggedInOnly`" v-bind="field"> Visible only to registered users </a-checkbox>
<error-message :name="field.name" />
</Field>
<Field :name="name + '.hidden'" type="checkbox" :unchecked-value="false" :value="true" v-slot="{ value, field, errorMessage }">
<a-tooltip>
<template #title> Hides your story from everyone except you and site admins. </template>
<a-checkbox v-bind="field"> Hidden </a-checkbox>
<a-checkbox v-bind="field" :data-testid="`${tbase}.hidden`"> Hidden </a-checkbox>
</a-tooltip>
</Field>
<a-row>
<a-col :span="24">
<genre />
<genre :data-testid="`${tbase}.genre`" />
</a-col>
</a-row>
<a-divider> Contents </a-divider>
<upload-or-paste />
<Field :name="name + '.id'" v-if="!!data.id" :model-value="data.id" />
<upload-or-paste :data-testid="`${tbase}.uploadOrPaste`" />
<Field :hidden="true" :name="name + '.id'" v-if="!!data.id" :model-value="data.id" />
</div>
</template>

@ -11,14 +11,15 @@
</script>
<template>
<a-radio-group v-model:value="pvalue">
<a-radio value="pasteOrType">Paste or type content</a-radio>
<a-radio value="upload">Upload a file</a-radio>
<a-radio value="pasteOrType" :data-testid="$attrs['data-testid'] + '.radio.pasteOrType'">Paste or type content</a-radio>
<a-radio value="upload" :data-testid="$attrs['data-testid'] + '.radio.upload'">Upload a file</a-radio>
</a-radio-group>
<br />
<br />
<base-editor label="" v-if="pvalue === 'pasteOrType'" :init="story" :name="fname + 'content'" />
<base-editor :data-testid="$attrs['data-testid'] + '.type'" label="" v-if="pvalue === 'pasteOrType'" :init="story" :name="fname + 'content'" />
<a-upload
v-model:file-list="fileList"
:data-testid="$attrs['data-testid'] + '.upload'"
v-else-if="pvalue === 'upload'"
:name="fname + 'file'"
accept=".doc,.docx,.md,.markdown"
@ -32,6 +33,7 @@
let resp;
try {
resp = bap.file.response;
console.log('RESPONSE', resp);
if (resp.success) {
fileField.setValue(resp.fileName);
console.log(fileField.value);

@ -1,6 +1,18 @@
const plugins =
"a11ychecker advcode advtable advtemplate typography casechange checklist editimage mediaembed export footnotes formatpainter inlinecss mergetags pageembed permanentpen powerpaste autocorrect tableofcontents accordion advlist anchor autolink autoresize autosave charmap code codesample directionality emoticons fullscreen help image importcss insertdatetime link lists media nonbreaking pagebreak preview quickbars save searchreplace table template visualblocks visualchars wordcount".split(
" ",
);
const reducer = (pv, cv) => {
return { ...pv, [cv]: `plugins/${cv}/plugin.min.js` };
};
const external_plugins = plugins.reduce((pv, cv) => {
return { ...pv, [cv]: `plugins/${cv}/plugin.min.js` };
}, {});
const base_url = "/tinymce";
export const fancy = {
branding: false,
selector: "textarea#txt",
base_url,
menubar: false,
elementpath: false,
formats: {
@ -31,12 +43,12 @@ export const fancy = {
items: [
{
title: "Image Left",
selector: "img",
styles: { float: "left", margin: "0 10px 0 10px" },
},
{
title: "Image Right",
selector: "img",
styles: { float: "right", margin: "0 0 10px 10px" },
},
{ title: "Rounded corners", styles: { "border-radius": "0.7em" } },
@ -44,7 +56,7 @@ export const fancy = {
},
{
title: "Code/Monospace",
selector: "p,div,h1,h2,h3,h4,h5,h6",
classes: ["font-mono"],
},
],
@ -62,16 +74,11 @@ export const fancy = {
},
toolbar: "undo redo | paste | bold italic underline | hr image link | forecolor styles | heading alignment | code",
contextmenu: "bold italic underline | hr | link | image | paste",
external_plugins: {
mentions: "/plugins/mentions/plugin.min.js",
},
plugins:
`advlist autolink lists link image charmap preview anchor searchreplace visualblocks advcode fullscreen insertdatetime media table help wordcount save mentions`.split(
" ",
),
external_plugins,
};
export const comment = {
branding: false,
base_url,
height: 200,
menubar: false,
elementpath: false,
@ -86,21 +93,15 @@ export const comment = {
inline: "u",
},
},
external_plugins: {
mentions: "/plugins/mentions/plugin.min.js",
},
schema: "html5",
plugins:
`advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table code help save mentions`.split(
" ",
),
external_plugins,
toolbar: "bold italic underline | link",
contextmenu: "bold italic underline | paste | link",
};
export const story = {
base_url,
branding: false,
selector: "textarea#txt",
height: 500,
menubar: false,
elementpath: false,
@ -117,14 +118,12 @@ export const story = {
},
content_css: ["/test.css", "/quickfix.css"],
schema: "html5",
plugins:
`advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table advcode help wordcount save`.split(
" ",
),
external_plugins,
toolbar: "undo redo | paste |" + "bold italic underline | hr | alignleft aligncenter " + "alignright alignjustify | " + "| code",
contextmenu: "bold italic underline | hr | paste | link",
};
export const bare = {
base_url,
branding: false,
height: 200,
menubar: false,
@ -141,13 +140,7 @@ export const bare = {
},
},
schema: "html5",
external_plugins: {
mentions: "/plugins/mentions/plugin.min.js",
},
plugins:
`advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table advcode help save mentions`.split(
" ",
),
external_plugins,
toolbar: "bold italic underline | hr link",
contextmenu: "bold italic underline | paste | hr link",
};

@ -26,8 +26,7 @@ export const storyMiddleware = defineNuxtRouteMiddleware(async (to, from) => {
export const storyEditMiddleware = defineNuxtRouteMiddleware(async (to, from) => {
const { data: curU } = useAuth();
const rtr = useRoute();
const { data: storyInfo } = await useApiFetch<({ chapters: (IChapter & { text: string })[] } & IStory) | null>(`/story/${rtr.params.id}/full`);
const { data: storyInfo } = await useApiFetch<({ chapters: (IChapter & { text: string })[] } & IStory) | null>(`/story/${to.params.id}/full`);
if (!storyInfo.value) show404();
if (curU.value?.user?._id !== (storyInfo.value?.author as IUser)._id && curU.value?.user?._id !== (storyInfo.value?.coAuthor as IUser)?._id) {
return showError({
@ -38,8 +37,7 @@ export const storyEditMiddleware = defineNuxtRouteMiddleware(async (to, from) =>
});
export const draftEditMiddleware = defineNuxtRouteMiddleware(async (to, from) => {
const { data: curU } = useAuth();
const rtr = useRoute();
const { data: storyInfo } = await useApiFetch<IDraft | null>(`/draft/${rtr.params.id}`);
const { data: storyInfo } = await useApiFetch<IDraft | null>(`/draft/${to.params.id}`);
if (!storyInfo.value) show404();
if (curU.value?.user?._id !== (storyInfo.value?.author as IUser)._id && curU.value?.user?._id !== (storyInfo.value?.coAuthor as IUser)?._id) {
return showError({

@ -1,16 +1,13 @@
import { resolve } from "path";
import * as dotenv from "dotenv";
import mongoose from "mongoose";
const FUCKYOU = "/rockfic/abc-rockfic-v3";
dotenv.config({ path: resolve("/home/rockfic/rockfic-staging/.env") });
export const hasMigrated = false;
console.log("indbconfig");
// dotenv.config({ path: resolve("/home/rockfic/rockfic-staging/.env") });
export const hasMigrated = true;
export const uri =
process.env.NODE_ENV === "production"
process.env.NODE_ENV === "production" && !process.env.TEST
? `mongodb://${process.env.DBUSER}:${process.env.DBPASS}@${process.env.DB}?authSource=admin`
: `mongodb://127.0.0.1/${process.env.DBNAME}?authSource=admin`;
: `mongodb://127.0.0.1/${process.env.DBNAME}`;
export const olduri =
process.env.NODE_ENV === "production"
process.env.NODE_ENV === "production" && !process.env.TEST
? `mongodb://${process.env.DBUSER}:${process.env.DBPASS}@127.0.0.1:27017/rockfic_old?authSource=admin`
: `mongodb://127.0.0.1/rockfic_old?authSource=admin`;

128
lib/testing/helpers.ts Normal file

@ -0,0 +1,128 @@
import { Page } from "playwright-core";
import { randomBytes } from "crypto";
import { resolve, join } from "path";
import { fireEvent } from "@testing-library/vue";
const sampleFiles = ["smut.md", "smut-2.md", "somno.md"];
export function rand(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
async function fillCharacters(page: Page, fieldId: string, count: number) {
const el = page.getByTestId(fieldId);
const inp = el.locator("input");
await inp.focus();
let existingIndices: number[] = [];
while (existingIndices.length <= count) {
let downPresses = rand(1, 4);
if (existingIndices.includes(downPresses)) {
continue;
}
existingIndices.push(downPresses);
}
for (let k = 0; k < existingIndices.length; k++) {
await inp.focus();
await inp.press("ArrowDown");
for (let l = 0; l < existingIndices[k]; l++) {
await inp.press("ArrowDown");
}
await inp.press("Escape");
}
await inp.press("Enter");
}
async function fillRelationships(page: Page, id: string) {
const add = page.getByTestId(`${id}.form.relationships.add`);
for (let i = 0; i < rand(1, 3); i++) {
await add.click();
const pairingMembers = rand(2, 3);
await fillCharacters(page, `${id}.form.relationships.${i}`, pairingMembers);
}
}
async function uploadOrPaste(page: Page, selector: string, index: number) {
const uop = rand(1, 2) == 1;
const innerSel = `${selector}.form.uploadOrPaste`;
if (uop) {
const fileChoosePromise = page.waitForEvent("filechooser");
const el = page.getByTestId(`${innerSel}.radio.upload`);
await el.click();
const ubtn = page.getByTestId(`${innerSel}.upload`);
await ubtn.click();
const fc = await fileChoosePromise;
await fc.setFiles(join(resolve(import.meta.dirname, "../../tests/inputFiles"), sampleFiles[rand(0, 2)]));
} else {
const el = page.getByTestId(`${innerSel}.radio.pasteOrType`);
await el.click();
const content = page.getByTestId(`${innerSel}.type`).frameLocator("iframe").locator("[contenteditable]");
await content.pressSequentially(
randomBytes(148)
.toString("base64")
.match(/.{1,2}/g)!
.join(" "),
);
await content.focus();
await content.press("Enter");
await content.press("Enter");
await content.pressSequentially(`--- end of chapter ${index + 1} ---`.toLocaleUpperCase());
}
}
export async function partialChapter(page: Page, i: number) {
const b = `storyform.chapters[${i}]`;
const collapse = page.getByTestId(`${b}.collapse`);
await collapse.click();
await page.waitForTimeout(3000);
const inp = page.getByTestId(`${b}.form.title`);
const summary = page.getByTestId(`${b}.form.summary`).frameLocator("iframe").locator("[contenteditable]");
const notes = page.getByTestId(`${b}.form.notes`).frameLocator("iframe").locator("[contenteditable]");
await inp.pressSequentially(`~ ${i + 1} ~`);
await summary.press("Control+A");
await summary.press("Backspace");
await summary.pressSequentially(
randomBytes(25)
.toString("hex")
.match(/.{1,2}/g)!
.join(" "),
);
await notes.pressSequentially("these are some notes. Brian Tatler fucked and abused Sean Harris!");
await notes.focus();
await notes.press("Enter");
await notes.press("Enter");
await notes.pressSequentially("0x" + i.toString(16));
}
export async function fillChapter(page: Page, i: number) {
const b = `storyform.chapters[${i}]`;
const nsfwBool = rand(1, 2) == 2;
const bands = page.getByTestId(`${b}.form.bands`);
const bandInput = bands.locator("input");
const genre = page.getByTestId(`${b}.form.genre`);
const genreInput = genre.locator("input");
await partialChapter(page, i);
await bandInput.pressSequentially("Diamond Head");
await bandInput.press("Enter");
await bandInput.press("Escape");
await fillCharacters(page, `${b}.form.characters`, rand(2, 4));
await fillRelationships(page, b);
const nsfw = page.getByTestId(`${b}.form.nsfw`);
nsfwBool && (await nsfw.click());
nsfwBool && (await page.getByTestId(`${b}.form.loggedInOnly`).click());
await genreInput.focus();
await genreInput.pressSequentially("S");
await genreInput.press("Enter");
await uploadOrPaste(page, b, i);
}
export async function collapseSidebar(page: Page) {
await page.locator(".ant-layout-sider-zero-width-trigger").click();
}
// vue stuff

@ -25,6 +25,7 @@
"@sidebase/nuxt-auth": "0.6.0-beta.6",
"@tinymce/tinymce-vue": "^5.1.1",
"@types/jsonwebtoken": "^9.0.3",
"@types/lodash-es": "^4.17.12",
"@types/turndown": "^5.0.4",
"@vueuse/core": "^10.4.1",
"@vueuse/nuxt": "^10.4.1",
@ -41,17 +42,19 @@
"mongodb": "^6.1.0",
"mongoose": "^8.0.2",
"mongoose-sequence": "https://github.com/amansingh63/mongoose-sequence",
"nitropack": "^2.9.4",
"nuxi": "^3.10.0",
"nuxt-security": "^0.14.4",
"nuxt-speedkit": "3.0.0-next.26",
"pinia": "^2.1.6",
"sanitize-html": "^2.11.0",
"sharp": "^0.33.1",
"sharp": "^0.33.2",
"string-strip-html": "^13.4.3",
"tinymce": "^6.7.0",
"turndown": "^7.1.2",
"uuid": "^9.0.1",
"vee-validate": "^4.11.7",
"vue": "^3.4.4",
"vue": "^3.4.21",
"vue-dndrop": "^1.3.1",
"vue-recaptcha": "3.0.0-alpha.6",
"vuedraggable": "^4.1.0",

@ -2,6 +2,9 @@
import storyForm from "~/components/story/create/storyForm.vue";
import { defaultStory } from "@client/types/form/story";
const ds = ref(defaultStory);
const { getSession } = useAuth();
await getSession({ force: true });
definePageMeta({
middleware: ["auth"],
});

@ -1 +1 @@
Subproject commit bffc9f1f85710ea8c62151770f0ef58e51692826
Subproject commit bcdaae9743a62dc4172a117471bd3a49462d90be

@ -7,9 +7,9 @@ export default eventHandler(async (event) => {
const wrongMsg = "wrong credentials";
let reqbody = await readBody(event);
let user = await User.findOne({ username: reqbody.username }).exec();
log.debug(reqbody, { label: "login/body" });
log.debug("USER -> " + user, { label: "login" });
log.debug("conn ->" + mongoose.connection, { label: "login" });
// log.debug(reqbody, { label: "login/body" });
// log.debug("USER -> " + user, { label: "login" });
// log.debug("conn ->" + mongoose.connection, { label: "login" });
let cok = getHeader(event, "Authorization")?.replace("Bearer ", "");
if (!cok) {
if (!user) {