diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4e1cc60 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:23-alpine AS frontend +WORKDIR /frontend +COPY frontend/package*.json /frontend +RUN npm ci +RUN npm i @tailwindcss/oxide-linux-x64-musl +COPY frontend/ /frontend +RUN npm run build + +FROM golang:1.24-alpine AS backend +WORKDIR /backend +COPY backend/go.* /backend/ +RUN go mod download +COPY backend/ /backend +COPY --from=frontend /frontend/dist/client/ /backend/public/ +RUN go build -o HexDeck + +FROM scratch +WORKDIR /app +COPY --from=backend /backend/HexDeck . +CMD ["/app/HexDeck"] \ No newline at end of file diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..3320515 --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1 @@ +HexDeck \ No newline at end of file diff --git a/backend/api/api.go b/backend/api/api.go index f273bd0..265c685 100644 --- a/backend/api/api.go +++ b/backend/api/api.go @@ -1,16 +1,12 @@ package api import ( - "fmt" - "log" "log/slog" "net/http" - "strconv" "github.com/HexCardGames/HexDeck/db" "github.com/HexCardGames/HexDeck/game" "github.com/HexCardGames/HexDeck/types" - "github.com/HexCardGames/HexDeck/utils" "github.com/gin-gonic/gin" ) @@ -37,10 +33,7 @@ type LeaveRoomRequest struct { SessionToken string } -func InitApi() { - server := gin.Default() - server.SetTrustedProxies(nil) - +func RegisterApi(server *gin.Engine) { server.GET("/api/stats", func(c *gin.Context) { stats := game.CalculateStats() c.JSON(http.StatusOK, StatsReply{ @@ -142,12 +135,4 @@ func InitApi() { // Handle WebSocket connections using Socket.io wsHandler := initWS() server.Any("/socket.io/", gin.WrapH(wsHandler)) - - listenHost := utils.Getenv("LISTEN_HOST", "0.0.0.0") - listenPort, err := strconv.Atoi(utils.Getenv("LISTEN_PORT", "3000")) - if err != nil { - log.Fatal("Value of variable PORT is not a valid integer!") - } - slog.Info(fmt.Sprintf("HexDeck server listening on http://%s:%d", listenHost, listenPort)) - server.Run(fmt.Sprintf("%s:%d", listenHost, listenPort)) } diff --git a/backend/api/static.go b/backend/api/static.go new file mode 100644 index 0000000..944b634 --- /dev/null +++ b/backend/api/static.go @@ -0,0 +1,26 @@ +package api + +import ( + "embed" + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func SPAMiddleware(fs embed.FS, prefix string, notFoundPath string) gin.HandlerFunc { + fileServer := http.FileServerFS(fs) + + return func(c *gin.Context) { + if strings.HasPrefix(c.Request.URL.Path, "/api/") { + c.Next() + return + } + c.Request.URL.Path = prefix + c.Request.URL.Path + _, err := fs.Open(c.Request.URL.Path) + if err != nil { + c.Request.URL.Path = prefix + notFoundPath + } + fileServer.ServeHTTP(c.Writer, c.Request) + } +} diff --git a/backend/db/db.go b/backend/db/db.go index 93356aa..5ad94c6 100644 --- a/backend/db/db.go +++ b/backend/db/db.go @@ -70,13 +70,22 @@ func (conn *DatabaseConnection) QueryGlobalStats() GlobalStatsCollection { return stats } -func CreateDBConnection(uri string) DatabaseConnection { - client, _ := mongo.Connect(options.Client().ApplyURI(uri)) - return DatabaseConnection{client} +func CreateDBConnection(uri string) *DatabaseConnection { + client, err := mongo.Connect(options.Client().ApplyURI(uri)) + if err != nil { + slog.Error("MongoDB connection failed", "error", err) + return nil + } + return &DatabaseConnection{client} } var Conn DatabaseConnection -func InitDB(uri string) { - Conn = CreateDBConnection(uri) +func InitDB(uri string) bool { + dbConn := CreateDBConnection(uri) + if dbConn == nil { + return false + } + Conn = *dbConn + return true } diff --git a/backend/main.go b/backend/main.go index 7537209..3880635 100644 --- a/backend/main.go +++ b/backend/main.go @@ -1,16 +1,24 @@ package main import ( + "embed" + "fmt" + "log" "log/slog" "os" + "strconv" "time" "github.com/HexCardGames/HexDeck/api" "github.com/HexCardGames/HexDeck/db" "github.com/HexCardGames/HexDeck/game" "github.com/HexCardGames/HexDeck/utils" + "github.com/gin-gonic/gin" ) +//go:embed all:public/* +var public embed.FS + func main() { logHandler := slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: slog.LevelDebug, @@ -22,7 +30,11 @@ func main() { slog.Error("MONGO_URI environment variable not set!") return } - db.InitDB(mongoUri) + ok := db.InitDB(mongoUri) + if !ok { + slog.Error("Initializing MongoDB database failed") + return + } game.LoadRooms() roomTicker := time.NewTicker(1 * time.Second) @@ -35,5 +47,17 @@ func main() { } }() - api.InitApi() + server := gin.Default() + server.SetTrustedProxies(nil) + + api.RegisterApi(server) + server.Use(api.SPAMiddleware(public, "public", "/")) + + listenHost := utils.Getenv("LISTEN_HOST", "0.0.0.0") + listenPort, err := strconv.Atoi(utils.Getenv("LISTEN_PORT", "3000")) + if err != nil { + log.Fatal("Value of variable PORT is not a valid integer!") + } + slog.Info(fmt.Sprintf("HexDeck server listening on http://%s:%d", listenHost, listenPort)) + server.Run(fmt.Sprintf("%s:%d", listenHost, listenPort)) } diff --git a/backend/public/README.md b/backend/public/README.md new file mode 100644 index 0000000..6e6c3b1 --- /dev/null +++ b/backend/public/README.md @@ -0,0 +1,7 @@ +# Static Frontend Hosting + +All files in this directory will be merged into the server binary and served as static assets by the HexDeck server. + +When building with docker, the compiled frontend will be copied here automatically. + +If you are building manually, you can copy the contents of the `dist/client/` folder here after building the frontend. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..87d4a22 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + mongodb: + user: "1000" + volumes: + - ./data/mongodb/:/data/db/ + image: mongodb/mongodb-community-server:latest + command: mongod --bind_ip_all + hexdeck: + user: "1000" + ports: + - 3000:3000 + environment: + - MONGO_URI=mongodb://mongodb:27017/ + depends_on: + - mongodb + build: . \ No newline at end of file diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..f4bbd82 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,26 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +.routify/* + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? \ No newline at end of file