diff --git a/Dockerfile b/Dockerfile index e5f72e2..5cba9c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,15 +12,17 @@ ARG NUXT_APP_BASE_URL=/ ENV NUXT_APP_BASE_URL=$NUXT_APP_BASE_URL RUN pnpm build -RUN cp -r .output /app/SERVER_OUTPUT +RUN cp -rL .output /app/SERVER_OUTPUT RUN pnpm generate -RUN cp -r .output/public /app/CLIENT_OUTPUT +RUN cp -rL .output/public /app/CLIENT_OUTPUT RUN mkdir /app/SERVER_OUTPUT/public/node_modules && \ mkdir /app/CLIENT_OUTPUT/node_modules && \ - cp -r node_modules/monaco-editor-workers /app/SERVER_OUTPUT/public/node_modules/monaco-editor-workers && \ - cp -r node_modules/monaco-editor /app/CLIENT_OUTPUT/node_modules/monaco-editor + cp -rL node_modules/monaco-editor-workers /app/SERVER_OUTPUT/public/node_modules/monaco-editor-workers && \ + cp -rL node_modules/monaco-editor /app/SERVER_OUTPUT/public/node_modules/monaco-editor && \ + cp -rL node_modules/monaco-editor-workers /app/CLIENT_OUTPUT/node_modules/monaco-editor-workers && \ + cp -rL node_modules/monaco-editor /app/CLIENT_OUTPUT/node_modules/monaco-editor ### Production Image FROM node:20-alpine diff --git a/components/nav-bar.vue b/components/nav-bar.vue index b1c3602..9673b32 100644 --- a/components/nav-bar.vue +++ b/components/nav-bar.vue @@ -3,10 +3,28 @@ + + +

not-th.re @@ -43,6 +61,13 @@ const props = defineProps<{ const expiresObject = ref({ hours: 0, minutes: 0, seconds: 0 }) const expiresString = ref('XX:XX:XX') const scheduler = ref(0) +const maxExpireDays = ref(30) +const expiresCustomTime = ref(30) + +watch(() => props.defaultExpires, (value) => { + maxExpireDays.value = Math.floor(value / 1000 / 60 / 60 / 24) + expiresCustomTime.value = maxExpireDays.value +}, { immediate: true }) watch(() => props.expires, (value) => { if (!props.expires) { @@ -84,7 +109,7 @@ onUnmounted(() => { clearTimeout(scheduler.value) }) -const visible = ref(false) +const visible = ref(0) const route = useRoute() const warning = [ 'With this url the server will be able to decrypt your data.', @@ -97,6 +122,7 @@ const entries = computed(() => ([ name: 'file', entries: [ ['save', 'Save for ' + Math.floor(props.defaultExpires / 1000 / 60 / 60 / 24) + ' days'], + ['save-custom', 'Save for custom time'], ['duplicate', 'Duplicate'], ['new', 'New'], ] as [string, string][], @@ -131,7 +157,7 @@ function getBaseURL() { function copyDecrypted() { const url = getBaseURL(); navigator.clipboard.writeText(`${url}decrypt/${route.params.id}/${location.hash.substring(1)}`) - visible.value = false + visible.value = 0 } function handle(entry: string) { @@ -148,7 +174,7 @@ function handle(entry: string) { navigator.clipboard.writeText(`${url}raw/${route.params.id}`) break case 'share-decrypted': - visible.value = true + visible.value = 1 break case 'github': window.open('https://github.com/not-three/main', '_blank') @@ -160,9 +186,20 @@ function handle(entry: string) { console.log(JSON.stringify(props.config)) window.open(props.config.terms, '_blank') break + case 'save-custom': + visible.value = 2 + break default: if (!['save', 'duplicate', 'new'].includes(entry)) throw new Error('Invalid entry') emit(entry as any); } } + +function saveCustomTime() { + const days = expiresCustomTime.value; + if (days < 0 || days > maxExpireDays.value) return; + expiresCustomTime.value = days; + emit('save', days * 24 * 60 * 60 * 1000); + visible.value = 0; +} diff --git a/components/yes-no.vue b/components/yes-no.vue index 7ce300f..ab412fe 100644 --- a/components/yes-no.vue +++ b/components/yes-no.vue @@ -14,6 +14,7 @@

{{ props.message }}

+
@@ -50,6 +56,7 @@ const props = defineProps<{ message: string; disableNo?: boolean; altYes?: string; + altNo?: string; }>(); const emits = defineEmits(['yes', 'no']) watch(() => props.visible, (value) => { diff --git a/mixins/base.ts b/mixins/base.ts index 34bbb3f..4893c8f 100644 --- a/mixins/base.ts +++ b/mixins/base.ts @@ -10,14 +10,16 @@ export default defineNuxtComponent({ api: null as any, errorVisible: false, errorMessage: '', - defaultExpires: 0, + defaultExpires: 1, }), async mounted() { const handler = (event: any) => { + if (this.readOnly) return; if (event.origin !== location.origin) return; if (event.data?.type !== 'DUPLICATE_SHARE') return; this.content = event.data.content; window.removeEventListener('message', handler); + event.source.postMessage({ type: 'DUPLICATE_SHARE_OK' }, event.origin); } window.addEventListener('message', handler); const api = await this.getApi() @@ -35,12 +37,15 @@ export default defineNuxtComponent({ this.errorMessage = message; this.errorVisible = true; }, - async saveD() { + async saveD(expires?: number) { if (this.readOnly) return this.showError('Cannot save readonly note'); if (!this.content) return this.showError('No content to save'); const secret = Math.random().toString(36).substring(2); const encrypted = CryptoJS.AES.encrypt(this.content, secret).toString(); - const res = await (await this.getApi()).post('create', { content: encrypted }); + const res = await (await this.getApi()).post('create', { + ...(expires ? { expires } : {}), + content: encrypted, + }); // window.location.href = `/q/${res.data.id}#${secret}`; this.$router.push('/q/' + res.data.id + '#' + secret); }, @@ -48,13 +53,18 @@ export default defineNuxtComponent({ if (!this.content) return this.showError('No content to duplicate'); const win = window.open(location.origin, '_blank'); win?.addEventListener('load', async () => { - let counter = 0; - let interval = window.setInterval(() => { - if (counter++ > 200) { - window.clearInterval(interval); - } - win?.postMessage({ type: 'DUPLICATE_SHARE', content: this.content }, location.origin); - }, 100); + let ok = false; + let tries = 0; + win.addEventListener('message', (event) => { + if (event.origin !== location.origin) return; + if (event.data?.type !== 'DUPLICATE_SHARE_OK') return; + ok = true; + }); + while (!ok && tries < 200) { + win.postMessage({ type: 'DUPLICATE_SHARE', content: this.content }, location.origin); + tries++; + await new Promise(r => setTimeout(r, 100)); + } }); }, async newD() { @@ -62,7 +72,7 @@ export default defineNuxtComponent({ }, async getApi(): Promise { if (this.api) return this.api; - this.configData = (await Axios.get('/config.json')).data; + this.configData = (await Axios.get(this.$config.app.baseURL + 'config.json')).data; this.api = Axios.create({ baseURL: this.configData.baseURL, }) diff --git a/package.json b/package.json index 5457426..cfba2d0 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "module", "scripts": { "build": "nuxt build", - "dev": "USE_SQLITE=true nuxt dev", + "dev": "IP_HEADER=false USE_SQLITE=true nuxt dev", "generate": "SKIP_DB=true nuxt generate", "preview": "nuxt preview", "postinstall": "nuxt prepare", diff --git a/pages/q/[id].vue b/pages/q/[id].vue index d4d9a26..74061ec 100644 --- a/pages/q/[id].vue +++ b/pages/q/[id].vue @@ -48,13 +48,13 @@ export default defineNuxtComponent({ }), async mounted() { try { + this.readOnly = true; const api = await this.getApi(); const secret = location.hash.substring(1); this.decryptURL = api.defaults.baseURL + `decrypt/${this.$route.params.id}/${secret}`; const res = await api.get(`json/${this.$route.params.id}`); this.content = CryptoJS.AES.decrypt(res.data.content, secret).toString(CryptoJS.enc.Utf8); this.isReady = true; - this.readOnly = true; this.expires = res.data.expires; } catch (e) { console.error(e); diff --git a/server/api/create.post.ts b/server/api/create.post.ts index e17f530..2d83509 100644 --- a/server/api/create.post.ts +++ b/server/api/create.post.ts @@ -4,6 +4,7 @@ import { nanoid } from 'nanoid' export default defineEventHandler(async (event) => { const db = event.context.db as KnexInstance; const body = await readBody(event); + const defaultExpires = 1000 * (Number(process.env.EXPIRY) || 60 * 60 * 24 * 30); if (typeof body.content !== 'string') { setResponseStatus(event, 400); return {success: false, error: 'Invalid content'} @@ -16,7 +17,21 @@ export default defineEventHandler(async (event) => { setResponseStatus(event, 400); return {success: false, error: 'Content too short'} } - const realIP = event.node.req.connection.remoteAddress; + if (body.expires) { + if (typeof body.expires !== 'number') { + setResponseStatus(event, 400); + return {success: false, error: 'Invalid expiry'} + } + if (body.expires < 30_000) { + setResponseStatus(event, 400); + return {success: false, error: 'Expiry too short'} + } + if (body.expires > defaultExpires) { + setResponseStatus(event, 400); + return {success: false, error: 'Expiry too long'} + } + } + const realIP = event.node.req.socket.remoteAddress || '127.0.0.1'; const ipEnv = process.env.IP_HEADER; const ip = ipEnv !== 'false' ? event.node.req.headers[ipEnv || 'x-real-ip'] || realIP : realIP; const last60Minutes = new Date(Date.now() - (1000 * 60 * 60)); @@ -31,7 +46,7 @@ export default defineEventHandler(async (event) => { id, ip, content: body.content, created_at: db.fn.now(), - expires_at: new Date(Date.now() + (1000 * (Number(process.env.EXPIRY) || 60 * 60 * 24 * 30))), + expires_at: new Date(Date.now() + (body.expires || defaultExpires)), }); return {success: true, id}; }) diff --git a/server/api/status.get.ts b/server/api/status.get.ts index 04a6d93..626d537 100644 --- a/server/api/status.get.ts +++ b/server/api/status.get.ts @@ -16,6 +16,6 @@ export default defineEventHandler(async (event) => { } return { success: true, - count: Object.entries(count as any)[0][1] as number, + count: Number(Object.entries(count as any)[0][1]), }; });