From 4c396ef30fea895b12f56c56d0bf513b38f0c940 Mon Sep 17 00:00:00 2001 From: Adam French Date: Mon, 9 Mar 2026 13:47:38 +0000 Subject: [PATCH] Add file upload to website and integrate into chat --- backend/main.go | 1 + backend/models/models.go | 1 + docker-compose.yml | 7 +++++++ nginx/entrypoint.sh | 3 +++ nginx/nginx.conf.template | 9 +++++++++ nginx/nginx_dev.conf.template | 9 +++++++++ nginx/vue/src/components/util/Chat.vue | 25 ++++++++++++++++++++++++- nginx/vue/src/stores/messages.js | 15 +++++++++++++++ 8 files changed, 69 insertions(+), 1 deletion(-) diff --git a/backend/main.go b/backend/main.go index 98811cb..31a2bed 100644 --- a/backend/main.go +++ b/backend/main.go @@ -112,6 +112,7 @@ func main() { // MESSAGES r.GET("/ws", store.ConnectWebSocket) + r.POST("/messages/upload", store.UploadMessageFile) // NOTES r.GET("/notes/*path", store.GetNoteFile) diff --git a/backend/models/models.go b/backend/models/models.go index 306a0b4..9290657 100644 --- a/backend/models/models.go +++ b/backend/models/models.go @@ -31,6 +31,7 @@ type Message struct { ID uint `gorm:"primarykey" json:"id"` Content string `json:"text"` AuthorID uint `json:"authorId"` + FileURL string `json:"fileUrl,omitempty"` CreatedAt time.Time `json:"createdAt"` DeletedAt gorm.DeletedAt `gorm:"index" json:"deletedAt"` } diff --git a/docker-compose.yml b/docker-compose.yml index afbfb65..4ff8480 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,7 @@ networks: volumes: dbdata: + uploads: services: nginx: @@ -25,6 +26,7 @@ services: volumes: - ./certbot/conf:/etc/letsencrypt - ./certbot/www:/var/www/certbot + - uploads:/uploads certbot: image: certbot/certbot @@ -55,6 +57,7 @@ services: - ./backend/token/:/backend/token - ${OBSIDIAN_DIR}:/backend/notes - ./logs:/backend/logs + - uploads:/backend/uploads db: image: postgres:16 @@ -85,6 +88,8 @@ services: gitea-runner: image: gitea/act_runner:latest container_name: "${GITEA_RUNNER_HOST}" + profiles: + - disabled environment: GITEA_RUNNER_NAME: ${GITEA_RUNNER_NAME} CONFIG_FILE: /config.yaml @@ -110,6 +115,8 @@ services: - GITEA__database__NAME=${POSTGRES_GITEA_DB} - GITEA__database__USER=${POSTGRES_USER} - GITEA__database__PASSWD=${POSTGRES_PASSWORD} + - USER_UID=1000 + - USER_GID=1000 restart: always volumes: - ./gitea/data:/var/lib/gitea diff --git a/nginx/entrypoint.sh b/nginx/entrypoint.sh index 221cfdf..180f55a 100755 --- a/nginx/entrypoint.sh +++ b/nginx/entrypoint.sh @@ -17,5 +17,8 @@ else envsubst '${DOMAIN}' < /etc/nginx/nginx_setup.conf.template > /etc/nginx/nginx.conf fi +# Ensure uploads are readable by nginx worker processes +chmod -R a+rX /uploads 2>/dev/null || true + # Start nginx nginx -g 'daemon off;' diff --git a/nginx/nginx.conf.template b/nginx/nginx.conf.template index 3a8a4b0..a0b6b6d 100644 --- a/nginx/nginx.conf.template +++ b/nginx/nginx.conf.template @@ -9,6 +9,8 @@ http { server_tokens off; charset utf-8; + client_max_body_size 10M; + log_format compact '$remote_addr "$request" $status rt=$request_time'; @@ -55,6 +57,13 @@ http { ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; + location /uploads/ { + alias /uploads/; + add_header X-Content-Type-Options nosniff always; + add_header Content-Disposition "inline" always; + add_header Content-Security-Policy "default-src 'none'; img-src 'self'; style-src 'none'; script-src 'none'" always; + } + location / { try_files $uri $uri/ /index.html; } diff --git a/nginx/nginx_dev.conf.template b/nginx/nginx_dev.conf.template index 0bffc48..9049232 100644 --- a/nginx/nginx_dev.conf.template +++ b/nginx/nginx_dev.conf.template @@ -9,6 +9,8 @@ http { server_tokens off; charset utf-8; + client_max_body_size 10M; + log_format compact '$remote_addr "$request" $status rt=$request_time'; @@ -25,6 +27,13 @@ http { root /etc/nginx/html; index index.html; + location /uploads/ { + alias /uploads/; + add_header X-Content-Type-Options nosniff always; + add_header Content-Disposition "inline" always; + add_header Content-Security-Policy "default-src 'none'; img-src 'self'; style-src 'none'; script-src 'none'" always; + } + location / { try_files $uri $uri/ /index.html; } diff --git a/nginx/vue/src/components/util/Chat.vue b/nginx/vue/src/components/util/Chat.vue index 01ff77d..50ecc34 100644 --- a/nginx/vue/src/components/util/Chat.vue +++ b/nginx/vue/src/components/util/Chat.vue @@ -8,6 +8,7 @@ const messagesStore = useMessagesStore(); const messages = computed(() => messagesStore.messages); const messageInput = ref(""); const messagesContainer = ref(null); +const fileInput = ref(null); function scrollToBottom() { nextTick(() => { @@ -27,6 +28,17 @@ function sendMessage() { messageInput.value = ""; } +async function onFileSelected(e) { + const file = e.target.files[0]; + if (!file) return; + await messagesStore.uploadAndSendFile(file); + fileInput.value.value = ""; +} + +function isImageUrl(url) { + return /\.(jpg|jpeg|png|gif|webp)$/i.test(url); +} + onMounted(() => { messagesStore.connect(); }); @@ -45,9 +57,20 @@ onUnmounted(() => {

{{ message.authorId }}: {{ message.text }} +

- + +
+ + +
diff --git a/nginx/vue/src/stores/messages.js b/nginx/vue/src/stores/messages.js index 470ca4a..f444efb 100644 --- a/nginx/vue/src/stores/messages.js +++ b/nginx/vue/src/stores/messages.js @@ -1,5 +1,6 @@ import { defineStore } from "pinia"; import { ref, computed } from "vue"; +import axios from "axios"; function getWebSocketURL() { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; @@ -59,6 +60,19 @@ export const useMessagesStore = defineStore("messages", () => { messages.value = []; } + async function uploadAndSendFile(file) { + try { + const formData = new FormData(); + formData.append("file", file); + const res = await axios.post("/api/messages/upload", formData); + const { url } = res.data; + if (!socket.value || !isConnected.value) return; + socket.value.send(JSON.stringify({ text: "", fileUrl: url })); + } catch (err) { + lastError.value = err; + } + } + return { messages, isConnected, @@ -70,5 +84,6 @@ export const useMessagesStore = defineStore("messages", () => { disconnect, sendMessage, clearMessages, + uploadAndSendFile, }; });