Add local dev mode with HTTP-only nginx and DB seeding)
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 5m11s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 5m11s
This commit is contained in:
@@ -38,6 +38,9 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if os.Getenv("SEED_DB") == "true" {
|
||||||
|
services.SeedDatabase(db)
|
||||||
|
}
|
||||||
services.InitWebSocket(db)
|
services.InitWebSocket(db)
|
||||||
|
|
||||||
// SPOTIFY
|
// SPOTIFY
|
||||||
|
|||||||
65
backend/services/seed.go
Normal file
65
backend/services/seed.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
9
docker-compose.dev.yml
Normal file
9
docker-compose.dev.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
environment:
|
||||||
|
- DEV_MODE=true
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
certbot:
|
||||||
|
profiles:
|
||||||
|
- disabled
|
||||||
@@ -26,6 +26,7 @@ RUN mkdir -p /etc/nginx/html \
|
|||||||
|
|
||||||
COPY nginx.conf.template /etc/nginx/nginx.conf.template
|
COPY nginx.conf.template /etc/nginx/nginx.conf.template
|
||||||
COPY nginx_setup.conf.template /etc/nginx/nginx_setup.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 robots.txt /etc/nginx/html/robots.txt
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
# Check if certificate exists
|
# Check if dev mode, certificate exists, or setup mode
|
||||||
if [ -f "/etc/letsencrypt/live/$DOMAIN/fullchain.pem" ] && [ -f "/etc/letsencrypt/live/$DOMAIN/privkey.pem" ]; then
|
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."
|
echo "Certificates found. Using production nginx config."
|
||||||
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT}' \
|
envsubst '${DOMAIN} ${BACKEND_HOST} ${BACKEND_PORT} ${BACKEND_ENDPOINT} ${ICECAST_HOST} ${ICECAST_PORT} ${GITEA_HOST} ${GITEA_PORT}' \
|
||||||
< /etc/nginx/nginx.conf.template \
|
< /etc/nginx/nginx.conf.template \
|
||||||
|
|||||||
93
nginx/nginx_dev.conf.template
Normal file
93
nginx/nginx_dev.conf.template
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { ref, onMounted } from "vue";
|
|||||||
import Header from "@/components/text/Header.vue";
|
import Header from "@/components/text/Header.vue";
|
||||||
|
|
||||||
const url =
|
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 feed = ref(null);
|
||||||
const isLoading = ref(true);
|
const isLoading = ref(true);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const METRICS = [
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
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
|
rows.value = res.data.slice().reverse(); // API returns DESC, reverse to chronological
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error.value = e.message;
|
error.value = e.message;
|
||||||
|
|||||||
@@ -13,4 +13,11 @@ export default defineConfig({
|
|||||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": "http://localhost:8080",
|
||||||
|
"/gitea": "http://localhost:3000",
|
||||||
|
"/radio": "http://localhost:8000",
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
14
readme.md
14
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`.
|
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
|
## Future Ideas
|
||||||
|
|
||||||
- More Rust to WASM
|
- More Rust to WASM
|
||||||
@@ -96,6 +106,10 @@ BACKEND_HOST=
|
|||||||
BACKEND_SECRET=
|
BACKEND_SECRET=
|
||||||
BACKEND_ENDPOINT=
|
BACKEND_ENDPOINT=
|
||||||
|
|
||||||
|
CLAUDE_API_KEY=
|
||||||
|
|
||||||
|
SEED_DB=
|
||||||
|
|
||||||
OBSIDIAN_DIR=
|
OBSIDIAN_DIR=
|
||||||
|
|
||||||
SPOTIFY_CLIENT_ID=
|
SPOTIFY_CLIENT_ID=
|
||||||
|
|||||||
Reference in New Issue
Block a user