diff --git a/backend/main.go b/backend/main.go index 89d28e1..98811cb 100644 --- a/backend/main.go +++ b/backend/main.go @@ -38,6 +38,9 @@ func main() { if err != nil { log.Fatal(err) } + if os.Getenv("SEED_DB") == "true" { + services.SeedDatabase(db) + } services.InitWebSocket(db) // SPOTIFY diff --git a/backend/services/seed.go b/backend/services/seed.go new file mode 100644 index 0000000..f84515b --- /dev/null +++ b/backend/services/seed.go @@ -0,0 +1,65 @@ +package services + +import ( + "log" + "time" + + "adam-french.co.uk/backend/models" + "golang.org/x/crypto/bcrypt" + "gorm.io/gorm" +) + +func SeedDatabase(db *gorm.DB) { + var user models.User + if db.First(&user).Error == nil { + log.Println("Database already has data, skipping seed") + return + } + + log.Println("Seeding database with test data...") + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte("password"), bcrypt.DefaultCost) + if err != nil { + log.Fatal("Failed to hash seed password:", err) + } + + testUser := models.User{ + Username: "testuser", + Password: hashedPassword, + Admin: true, + } + db.Create(&testUser) + + posts := []models.Post{ + {Title: "Welcome to my blog", Content: "This is the first test post with some example content.", AuthorID: testUser.ID}, + {Title: "Learning Go", Content: "Go is a great language for building web servers and APIs.", AuthorID: testUser.ID}, + {Title: "Vue 3 Tips", Content: "The composition API makes Vue components much more flexible.", AuthorID: testUser.ID}, + } + db.Create(&posts) + + link1 := "https://example.com/project" + link2 := "https://example.com/book" + activities := []models.Activity{ + {Type: "project", Name: "coding"}, + {Type: "hobby", Name: "reading", Link: &link1}, + {Type: "fitness", Name: "exercise"}, + } + db.Create(&activities) + + favorites := []models.Favorite{ + {Type: "language", Name: "Go"}, + {Type: "book", Name: "Designing Data-Intensive Applications", Link: &link2}, + {Type: "framework", Name: "Vue"}, + } + db.Create(&favorites) + + now := time.Now() + rowingEntries := []models.Rowing{ + {Date: now.AddDate(0, 0, -14), Time: 1800, Distance: 5000, TimePer500m: 120.0, Calories: 300}, + {Date: now.AddDate(0, 0, -7), Time: 1750, Distance: 5200, TimePer500m: 118.5, Calories: 315}, + {Date: now, Time: 1700, Distance: 5400, TimePer500m: 116.2, Calories: 330}, + } + db.Create(&rowingEntries) + + log.Println("Database seeded successfully") +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..516ff64 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,9 @@ +services: + nginx: + environment: + - DEV_MODE=true + ports: + - 80:80 + certbot: + profiles: + - disabled diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 900db37..45c71b0 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -26,6 +26,7 @@ RUN mkdir -p /etc/nginx/html \ COPY nginx.conf.template /etc/nginx/nginx.conf.template COPY nginx_setup.conf.template /etc/nginx/nginx_setup.conf.template +COPY nginx_dev.conf.template /etc/nginx/nginx_dev.conf.template COPY robots.txt /etc/nginx/html/robots.txt COPY entrypoint.sh /entrypoint.sh diff --git a/nginx/entrypoint.sh b/nginx/entrypoint.sh index eb26814..221cfdf 100755 --- a/nginx/entrypoint.sh +++ b/nginx/entrypoint.sh @@ -1,8 +1,13 @@ #!/bin/sh set -e -# Check if certificate exists -if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then +# Check if dev mode, certificate exists, or setup mode +if [ "$DEV_MODE" = "true" ]; then + echo "Dev mode. Using HTTP-only nginx config." + envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT}' \ + < /etc/nginx/nginx_dev.conf.template \ + > /etc/nginx/nginx.conf +elif [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then echo "Certificates found. Using production nginx config." envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT}' \ < /etc/nginx/nginx.conf.template \ diff --git a/nginx/nginx_dev.conf.template b/nginx/nginx_dev.conf.template new file mode 100644 index 0000000..0bffc48 --- /dev/null +++ b/nginx/nginx_dev.conf.template @@ -0,0 +1,93 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server_tokens off; + charset utf-8; + + log_format compact + '$remote_addr "$request" $status rt=$request_time'; + + access_log /var/log/nginx/access.log compact; + + types { + text/javascript mjs; + } + + server { + listen 80; + server_name $DOMAIN www.$DOMAIN; + + root /etc/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location = /img/stamps/mine.gif { + add_header Access-Control-Allow-Origin *; + } + + location $BACKEND_ENDPOINT { + return 301 $BACKEND_ENDPOINT/; + } + + location $BACKEND_ENDPOINT/ws { + rewrite ^$BACKEND_ENDPOINT/(.*)$ /$1 break; + proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location $BACKEND_ENDPOINT/ { + rewrite ^$BACKEND_ENDPOINT/(.*)$ /$1 break; + proxy_pass http://$BACKEND_HOST:$BACKEND_PORT/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /radio { + return 301 /radio/; + } + + location /radio/ { + proxy_pass http://$ICECAST_HOST:$ICECAST_PORT/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /gitea { + return 301 /gitea/; + } + + location /gitea/ { + proxy_pass http://$GITEA_HOST:$GITEA_PORT/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + } + +} diff --git a/nginx/vue/src/components/util/CommitHistory.vue b/nginx/vue/src/components/util/CommitHistory.vue index b378082..4e4550c 100644 --- a/nginx/vue/src/components/util/CommitHistory.vue +++ b/nginx/vue/src/components/util/CommitHistory.vue @@ -4,7 +4,7 @@ import { ref, onMounted } from "vue"; import Header from "@/components/text/Header.vue"; const url = - "https://www.adam-french.co.uk/gitea/api/v1/users/adamf/activities/feeds?limit=1"; + "/gitea/api/v1/users/adamf/activities/feeds?limit=1"; const feed = ref(null); const isLoading = ref(true); diff --git a/nginx/vue/src/views/home/Gym2.vue b/nginx/vue/src/views/home/Gym2.vue index 973caa6..500a6fd 100644 --- a/nginx/vue/src/views/home/Gym2.vue +++ b/nginx/vue/src/views/home/Gym2.vue @@ -17,7 +17,7 @@ const METRICS = [ onMounted(async () => { try { - const res = await axios.get("https://www.adam-french.co.uk/api/rowing"); + const res = await axios.get("/api/rowing"); rows.value = res.data.slice().reverse(); // API returns DESC, reverse to chronological } catch (e) { error.value = e.message; diff --git a/nginx/vue/vite.config.js b/nginx/vue/vite.config.js index 8bd6c65..26d7634 100644 --- a/nginx/vue/vite.config.js +++ b/nginx/vue/vite.config.js @@ -13,4 +13,11 @@ export default defineConfig({ "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, + server: { + proxy: { + "/api": "http://localhost:8080", + "/gitea": "http://localhost:3000", + "/radio": "http://localhost:8000", + }, + }, }); diff --git a/readme.md b/readme.md index 854432b..0265953 100644 --- a/readme.md +++ b/readme.md @@ -61,6 +61,16 @@ certbot ── SSL Certificate Management Public endpoints for posts, users, favorites, activities, rowing, Spotify, notes, and WebSocket messaging. Protected endpoints for creating/updating/deleting content require JWT authentication via `/auth/login`. +## Local Testing (Dev Mode) + +Run the full stack over plain HTTP without SSL certificates: + +``` +docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build +``` + +This uses an HTTP-only nginx config with all routing (SPA, backend proxy, radio, gitea) and disables certbot. Visit `http://localhost` to test. + ## Future Ideas - More Rust to WASM @@ -96,6 +106,10 @@ BACKEND_HOST= BACKEND_SECRET= BACKEND_ENDPOINT= +CLAUDE_API_KEY= + +SEED_DB= + OBSIDIAN_DIR= SPOTIFY_CLIENT_ID=