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