Updated docker and server configs
This commit is contained in:
parent
eb39a175df
commit
b553cdc656
@ -1,5 +1,7 @@
|
||||
*
|
||||
!server
|
||||
server/node_modules
|
||||
!server/
|
||||
!client/
|
||||
server/node_modules/
|
||||
client/node_modules/
|
||||
!Dockerfile
|
||||
!.env
|
||||
|
48
.github/copilot-instructions.md
vendored
Normal file
48
.github/copilot-instructions.md
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
Do not run npm or create local node_modules in this repository
|
||||
============================================================
|
||||
|
||||
This repository is designed to run inside containers. Do NOT run `npm install`,
|
||||
`npm ci`, or otherwise modify `node_modules` or lockfiles locally on your
|
||||
machine. Doing so will create environment drift and can break container-based
|
||||
builds and CI.
|
||||
|
||||
Guidelines for contributors and automated assistants
|
||||
- Use the provided Docker and docker-compose workflows for development and
|
||||
building. The repository's `Dockerfile` and `docker-compose.yml` are the
|
||||
canonical way to install dependencies and run the app.
|
||||
- If you need to run a command locally for quick checks, prefer run commands in
|
||||
the project's container environment. Example (copy-paste):
|
||||
|
||||
```bash
|
||||
docker compose run --rm peddlers-of-ketran bash
|
||||
# inside container
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
- For lightweight commands, you can run one-off containers:
|
||||
|
||||
```bash
|
||||
docker run --rm -v "$PWD":/app -w /app node:20 bash -lc 'cd client && npm ci && npm run type-check'
|
||||
```
|
||||
|
||||
- Do not suggest or perform `npm install` or `npm ci` on the host machine in
|
||||
repository files, PRs, or automated edits. If a change requires dependency
|
||||
updates, update package.json and lockfiles via a CI job or in-container run,
|
||||
and record the change in a PR.
|
||||
|
||||
Copilot / Automation specific hints
|
||||
- When editing package.json, also update the appropriate lockfile using the
|
||||
container workflow and include lockfile changes in the same PR.
|
||||
- If a change requires running tests or builds, prefer to add a GitHub Action
|
||||
that runs the command inside the standard container image rather than
|
||||
instructing contributors to run commands locally.
|
||||
|
||||
Why this matters
|
||||
- Containers provide a reproducible environment and prevent developer
|
||||
machines from diverging. They ensure CI and production builds are consistent.
|
||||
|
||||
If you need a recommended local development setup or a dev container (VS Code
|
||||
Remote - Containers), create a `.devcontainer/` configuration and include it in
|
||||
the repo. That provides a supported local dev workflow that still uses
|
||||
containerized tooling.
|
32
.github/workflows/ci.yml
vendored
Normal file
32
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main, master ]
|
||||
pull_request:
|
||||
branches: [ main, master ]
|
||||
|
||||
jobs:
|
||||
server-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Server build and type-check (container)
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Server: Type-check and Build (inside node:20)
|
||||
run: |
|
||||
docker run --rm -v "$PWD":/work -w /work node:20 bash -lc '
|
||||
cd server && npm ci --no-audit --no-fund && npm run type-check && npm run build'
|
||||
|
||||
client-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Client type-check and build (container)
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Client: Type-check and Build (inside node:20)
|
||||
run: |
|
||||
docker run --rm -v "$PWD":/work -w /work node:20 bash -lc '
|
||||
cd client && npm ci --no-audit --no-fund && npm run type-check && npm run build'
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,4 +1,12 @@
|
||||
!.gitignore
|
||||
/.ssh/
|
||||
/client/node_modules/
|
||||
/server/node_modules/
|
||||
/node_modules/
|
||||
/dist/
|
||||
/build/
|
||||
.env
|
||||
.DS_Store
|
||||
/.vscode/!.gitignore
|
||||
!package.json
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
22
Dockerfile
22
Dockerfile
@ -1,4 +1,4 @@
|
||||
FROM ubuntu:jammy
|
||||
FROM ubuntu:noble
|
||||
|
||||
RUN apt-get -q update \
|
||||
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
|
||||
@ -10,7 +10,7 @@ RUN apt-get -q update \
|
||||
RUN mkdir -p /etc/apt/keyrings
|
||||
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
|
||||
|
||||
ENV NODE_MAJOR=20
|
||||
ENV NODE_MAJOR=22
|
||||
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
|
||||
|
||||
RUN apt-get -q update \
|
||||
@ -25,12 +25,26 @@ RUN apt-get -q update \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
|
||||
|
||||
COPY /server /server
|
||||
COPY server /server
|
||||
WORKDIR /server
|
||||
RUN npm install -s sqlite3
|
||||
RUN npm install
|
||||
RUN npm run build
|
||||
|
||||
# prepare client deps in the image so lint/type-check can run inside the container
|
||||
# copy client sources and install dependencies during the image build (container-first)
|
||||
COPY client /client
|
||||
WORKDIR /client
|
||||
ENV PUBLIC_URL="/ketr.ketran"
|
||||
ENV REACT_APP_API_BASE="/ketr.ketran"
|
||||
# prefer npm ci when lockfile present, otherwise fall back to npm install
|
||||
RUN rm -f package-lock.json
|
||||
RUN npm install --legacy-peer-deps --no-audit --no-fund
|
||||
RUN npm run build
|
||||
# return to server working dir for default run
|
||||
WORKDIR /server
|
||||
|
||||
COPY /Dockerfile /Dockerfile
|
||||
COPY /.env /.env
|
||||
|
||||
ENTRYPOINT [ "npm", "start" ]
|
||||
CMD ["npm", "start"]
|
||||
|
26
Dockerfile.server
Normal file
26
Dockerfile.server
Normal file
@ -0,0 +1,26 @@
|
||||
## Multi-stage Dockerfile for building the TypeScript server
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /
|
||||
|
||||
# Copy package files and install (in container only)
|
||||
COPY server/package*.json ./server/
|
||||
WORKDIR /server
|
||||
RUN npm ci --silent
|
||||
|
||||
# Copy server source and build
|
||||
COPY server/ ./
|
||||
RUN npm run build
|
||||
|
||||
## Production image
|
||||
FROM node:20-alpine AS runtime
|
||||
|
||||
WORKDIR /
|
||||
|
||||
# Copy built server
|
||||
COPY --from=builder /server/dist ./server/dist
|
||||
COPY server/package*.json /server/
|
||||
|
||||
WORKDIR /server
|
||||
ENV NODE_ENV=production
|
||||
EXPOSE 8930
|
||||
CMD ["npm", "start"]
|
239
README.md
239
README.md
@ -1,39 +1,115 @@
|
||||
# Peddlers of Ketran
|
||||
|
||||
This project consists of both the front-end and back-end game
|
||||
API server.
|
||||
This project consists of both the front-end React client and back-end Node.js game API server for the Settlers of Catan-style board game.
|
||||
|
||||
The front-end is launched from the 'client' directory in development mode via 'npm start'. In production, you build
|
||||
it via 'npm build' and deploy the public front-end.
|
||||
The application is designed to run inside Docker containers. Do not run `npm install` or modify `node_modules` locally, as this can create environment drift. All development and building should be done through the provided Docker workflows.
|
||||
|
||||
The back-end is launched out of the 'server' directory via 'npm start' and will
|
||||
bind to the default port 8930.
|
||||
## Development
|
||||
|
||||
If you change the default port of the REST API server, you will need to change
|
||||
client/package.json's "proxy" value to reflect the new port change.
|
||||
For live development with hot reloading, React DevTools support, and TypeScript compilation on-the-fly.
|
||||
|
||||
NOTE:
|
||||
### Prerequisites
|
||||
|
||||
Board.js currently hard codes assetsPath and gamesPath to be absolute as the
|
||||
dynamic router and resource / asset loading isn't working correctly.
|
||||
- Docker
|
||||
- Docker Compose
|
||||
|
||||
## Building
|
||||
### Running the Server (Backend)
|
||||
|
||||
### Native
|
||||
|
||||
#### Prerequisites
|
||||
The server uses TypeScript with hot reloading via `ts-node-dev`.
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y nodejs npm python
|
||||
sudo -E npm install --global npm@latest
|
||||
docker compose -f docker-compose.dev.yml up
|
||||
```
|
||||
|
||||
### In container
|
||||
This will:
|
||||
- Build the server container using `Dockerfile.server`
|
||||
- Mount the `server/` directory for live code changes
|
||||
- Run `npm run start:dev` which uses `ts-node-dev` for hot reloading
|
||||
- Expose the server on port 8930
|
||||
|
||||
### Testing the Server
|
||||
|
||||
The server uses Jest for unit testing. Tests are located in `server/tests/`.
|
||||
|
||||
To run tests:
|
||||
|
||||
```bash
|
||||
docker compose run --rm server npm test
|
||||
```
|
||||
|
||||
# Architecture
|
||||
This will run all test files matching `*.test.ts` in the `server/tests/` directory.
|
||||
|
||||
To run tests in watch mode:
|
||||
|
||||
```bash
|
||||
docker compose run --rm server npm run test:watch
|
||||
```
|
||||
|
||||
Note: If you add `test:watch` script, but for now, it's just `test`.
|
||||
|
||||
### Running the Client (Frontend)
|
||||
|
||||
The client is a React application with hot reloading and debug support.
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.client.dev.yml up
|
||||
```
|
||||
|
||||
This will:
|
||||
- Mount the `client/` directory for live code changes
|
||||
- Run `npm start` in development mode
|
||||
- Enable React DevTools and hot module replacement
|
||||
- Expose the client on port 3001 (HTTPS)
|
||||
- Proxy API calls to the server at `http://host.docker.internal:8930`
|
||||
|
||||
### Full Development Stack
|
||||
|
||||
To run both server and client together:
|
||||
|
||||
```bash
|
||||
# Terminal 1: Server
|
||||
docker compose -f docker-compose.dev.yml up
|
||||
|
||||
# Terminal 2: Client
|
||||
docker compose -f docker-compose.client.dev.yml up
|
||||
```
|
||||
|
||||
Open `https://localhost:3001` in your browser. The client will hot-reload on code changes, and the server will restart automatically on backend changes.
|
||||
|
||||
## Production
|
||||
|
||||
For production deployment, the client is built as static files and served directly by the Node.js server.
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
docker compose build
|
||||
```
|
||||
|
||||
This builds the production image using `Dockerfile`, which:
|
||||
- Installs server dependencies and builds TypeScript to `dist/`
|
||||
- Installs client dependencies and builds static files to `client/build/`
|
||||
- Copies the built client files to be served by the server
|
||||
|
||||
### Running
|
||||
|
||||
```bash
|
||||
docker compose up
|
||||
```
|
||||
|
||||
This will:
|
||||
- Start the server on port 8930
|
||||
- Serve the built client static files
|
||||
- Mount the `db/` directory for persistent database storage
|
||||
- Mount `server/routes/` for dynamic route files
|
||||
|
||||
The application will be available at `http://localhost:8930`.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file in the project root with any required environment variables. The server start script loads these via `export $(cat ../.env | xargs)`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```plantuml
|
||||
skinparam componentStyle rectangle
|
||||
@ -48,99 +124,40 @@ package "Game" as game {
|
||||
component Players as players
|
||||
}
|
||||
|
||||
server <-> resource : serves to client
|
||||
client <-> server
|
||||
server <-> resource : serves static files to client
|
||||
client <-> server : API calls
|
||||
player <-> client
|
||||
players -r-> player
|
||||
server -> game
|
||||
```
|
||||
|
||||
# Ketr.Ketran REST API
|
||||
## API Documentation
|
||||
|
||||
## POST /api/v1/game
|
||||
### POST /api/v1/game
|
||||
|
||||
### Request
|
||||
#### Request
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
### Response
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
gameId: id
|
||||
gameState: {
|
||||
tiles: []
|
||||
"gameId": "id",
|
||||
"gameState": {
|
||||
"tiles": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Configuring / installing
|
||||
## Configuration
|
||||
|
||||
## Build
|
||||
Add security tokens in `server/config/local.json`:
|
||||
|
||||
```bash
|
||||
git clone git.ketrenos.com:jketreno/peddlers-of-ketran.git
|
||||
cd server
|
||||
npm install
|
||||
npm start &
|
||||
cd ../client
|
||||
npm install
|
||||
npm start
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
export BASEPATH=${PWD}
|
||||
# Ensure sudo has password ready
|
||||
sudo -l
|
||||
sudo rsync -avprl install/ /etc/
|
||||
sudo touch /var/log/ketr-ketran.log
|
||||
sudo chown system:adm /var/log/ketr-ketran.log
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl restart rsyslogd
|
||||
sudo systemctl restart logrotate
|
||||
sudo systemctl restart ketr.ketran
|
||||
```
|
||||
|
||||
Install the following into your nginx server configuration:
|
||||
|
||||
```nginx
|
||||
location /ketr.ketran {
|
||||
root /var/www/ketrenos.com;
|
||||
index unresolvable-file-html.html;
|
||||
try_files $uri @index;
|
||||
}
|
||||
|
||||
# This seperate location is so the no cache policy only applies to the index and nothing else.
|
||||
location @index {
|
||||
root /var/www/ketrenos.com/ketr.ketran;
|
||||
add_header Cache-Control no-cache;
|
||||
expires 0;
|
||||
try_files /index.html =404;
|
||||
}
|
||||
|
||||
location /ketr.ketran/api {
|
||||
proxy_pass http://192.168.1.78:8930;
|
||||
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;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass_header Set-Cookie;
|
||||
proxy_pass_header P3P;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
```
|
||||
|
||||
Add security tokens in ketr.ketran/config/local.json:
|
||||
|
||||
```bash
|
||||
cat << EOF > config/local.json
|
||||
cat << EOF > server/config/local.json
|
||||
{
|
||||
"tokens": [ {
|
||||
"$(whoami)": "$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;)"
|
||||
@ -149,45 +166,25 @@ cat << EOF > config/local.json
|
||||
EOF
|
||||
```
|
||||
|
||||
## Launch
|
||||
## Testing
|
||||
|
||||
### Create New Game
|
||||
```bash
|
||||
sudo systemctl start ketr.ketran
|
||||
curl -X POST http://localhost:8930/api/v1/games/
|
||||
```
|
||||
|
||||
## To test
|
||||
|
||||
### New game
|
||||
### Get Game Status
|
||||
```bash
|
||||
curl -k -s -X POST http://localhost:8930/ketr.ketran/api/v1/games/
|
||||
curl -X GET http://localhost:8930/api/v1/games/:id
|
||||
```
|
||||
|
||||
### Game status
|
||||
## Game States
|
||||
|
||||
```bash
|
||||
curl -k -s -X GET http://localhost:8930/ketr.ketran/api/v1/games/:id
|
||||
```
|
||||
- **Lobby**: Players register, choose colors, roll for position, set ready
|
||||
- **Active**: Gameplay phase
|
||||
|
||||
# States
|
||||
Chat is available at all times for registered players.
|
||||
|
||||
Chat is available at all times by registered players
|
||||
## License Attribution
|
||||
|
||||
## Lobby
|
||||
|
||||
|
||||
* Register session+name
|
||||
* Register session with color
|
||||
* Unregister player+name from color
|
||||
* Roll dice for player position
|
||||
* Shuffle board
|
||||
* Set "Ready" for player
|
||||
* All ready? state == active
|
||||
|
||||
## License attribution
|
||||
|
||||
The dice faces (dice-six-faces-*.svg) are Copyright https://delapouite.com/ and licensed
|
||||
as [CC-BY-3.0](https://creativecommons.org/licenses/by/3.0/).
|
||||
|
||||
## Active
|
||||
|
||||
*
|
||||
The dice faces (dice-six-faces-*.svg) are Copyright [https://delapouite.com/](https://delapouite.com/) and licensed as [CC-BY-3.0](https://creativecommons.org/licenses/by/3.0/).
|
||||
|
22
docker-compose.client.dev.yml
Normal file
22
docker-compose.client.dev.yml
Normal file
@ -0,0 +1,22 @@
|
||||
services:
|
||||
peddlers-client:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: peddlers-client
|
||||
working_dir: /client
|
||||
volumes:
|
||||
- ./client:/client:rw
|
||||
ports:
|
||||
- 3001:3000
|
||||
environment:
|
||||
- BROWSER=none
|
||||
- HTTPS=true
|
||||
- HOST=0.0.0.0
|
||||
command: ["bash", "-c", "cd /client && npm install --legacy-peer-deps --silent --no-audit --no-fund && npm start"]
|
||||
networks:
|
||||
- peddlers-network
|
||||
|
||||
networks:
|
||||
peddlers-network:
|
||||
driver: bridge
|
21
docker-compose.dev.yml
Normal file
21
docker-compose.dev.yml
Normal file
@ -0,0 +1,21 @@
|
||||
services:
|
||||
peddlers-of-ketran:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
volumes:
|
||||
- ./server:/server:rw
|
||||
- ./db:/db:rw
|
||||
#- ./server/node_modules:app/server/node_modules:rw
|
||||
command: ["sh", "-c", "cd /server && npm install --no-audit --no-fund --silent && npm run start:dev"]
|
||||
ports:
|
||||
- 8930:8930
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
tty: true
|
||||
networks:
|
||||
- peddlers-network
|
||||
|
||||
networks:
|
||||
peddlers-network:
|
||||
driver: bridge
|
25
docker-compose.yml
Normal file
25
docker-compose.yml
Normal file
@ -0,0 +1,25 @@
|
||||
services:
|
||||
peddlers-of-ketran:
|
||||
container_name: ketr.ketran
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: always
|
||||
ports:
|
||||
- 8930:8930
|
||||
volumes:
|
||||
- ./db:/db:rw
|
||||
- ./server/routes:/server/routes:ro
|
||||
working_dir: /server
|
||||
peddlers-client:
|
||||
container_name: ketr.client
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
restart: 'no'
|
||||
working_dir: /client
|
||||
volumes:
|
||||
- ./client:/client:rw
|
||||
#- ./client:/client/node_modules:rw
|
||||
tty: true
|
||||
stdin_open: true
|
@ -1,4 +1,4 @@
|
||||
let basePath = process.env.REACT_APP_basePath;
|
||||
let basePath = process.env.REACT_APP_basePath || "";
|
||||
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
|
||||
if (basePath == "//") {
|
||||
basePath = "/";
|
||||
|
@ -1 +1,3 @@
|
||||
{}
|
||||
{
|
||||
"frontendPath": "/client/build"
|
||||
}
|
||||
|
@ -3,7 +3,12 @@
|
||||
"version": "1.0.0",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "export $(cat ../.env | xargs) && node app.js"
|
||||
"start": "export $(cat ../.env | xargs) && node dist/app.js",
|
||||
"start:legacy": "export $(cat ../.env | xargs) && node app.js",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"start:dev": "ts-node-dev --respawn --transpile-only src/app.ts",
|
||||
"test": "jest",
|
||||
"type-check": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"author": "James Ketrenos <james_settlers@ketrenos.com>",
|
||||
"license": "MIT",
|
||||
@ -30,6 +35,19 @@
|
||||
"typeface-roboto": "0.0.75",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/supertest": "^2.0.12",
|
||||
"jest": "^29.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "ts-jest"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@git.ketrenos.com:jketreno/peddlers-of-ketran"
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
const express = require("express"),
|
||||
router = express.Router(),
|
||||
crypto = require("crypto"),
|
||||
{ readFile, writeFile } = require("fs").promises,
|
||||
crypto = require("crypto")
|
||||
const { readFile, writeFile, mkdir } = require("fs").promises,
|
||||
fs = require("fs"),
|
||||
accessSync = fs.accessSync,
|
||||
randomWords = require("random-words"),
|
||||
@ -619,7 +619,7 @@ const loadGame = async (id) => {
|
||||
return games[id];
|
||||
}
|
||||
|
||||
let game = await readFile(`games/${id}`)
|
||||
let game = await readFile(`/db/games/${id}`)
|
||||
.catch(() => {
|
||||
return;
|
||||
});
|
||||
@ -627,20 +627,20 @@ const loadGame = async (id) => {
|
||||
if (game) {
|
||||
try {
|
||||
game = JSON.parse(game);
|
||||
console.log(`${info}: Creating backup of games/${id}`);
|
||||
await writeFile(`games/${id}.bk`, JSON.stringify(game));
|
||||
console.log(`${info}: Creating backup of /db/games/${id}`);
|
||||
await writeFile(`/db/games/${id}.bk`, JSON.stringify(game));
|
||||
} catch (error) {
|
||||
console.log(`Load or parse error from games/${id}:`, error);
|
||||
console.log(`Attempting to load backup from games/${id}.bk`);
|
||||
game = await readFile(`games/${id}.bk`)
|
||||
console.log(`Load or parse error from /db/games/${id}:`, error);
|
||||
console.log(`Attempting to load backup from /db/games/${id}.bk`);
|
||||
game = await readFile(`/db/games/${id}.bk`)
|
||||
.catch(() => {
|
||||
console.error(error, game);
|
||||
});
|
||||
if (game) {
|
||||
try {
|
||||
game = JSON.parse(game);
|
||||
console.log(`Saving backup to games/${id}`);
|
||||
await writeFile(`games/${id}`, JSON.stringify(game, null, 2));
|
||||
console.log(`Saving backup to /db/games/${id}`);
|
||||
await writeFile(`/db/games/${id}`, JSON.stringify(game, null, 2));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
game = null;
|
||||
@ -3448,15 +3448,16 @@ const saveGame = async (game) => {
|
||||
/* Save per turn while debugging... */
|
||||
game.step = game.step ? game.step : 0;
|
||||
/*
|
||||
await writeFile(`games/${game.id}.${game.step++}`, JSON.stringify(reducedGame, null, 2))
|
||||
await writeFile(`/db/games/${game.id}.${game.step++}`, JSON.stringify(reducedGame, null, 2))
|
||||
.catch((error) => {
|
||||
console.error(`${session.id} Unable to write to games/${game.id}`);
|
||||
console.error(`${session.id} Unable to write to /db/games/${game.id}`);
|
||||
console.error(error);
|
||||
});
|
||||
*/
|
||||
await writeFile(`games/${game.id}`, JSON.stringify(reducedGame, null, 2))
|
||||
await mkdir('/db/games', { recursive: true });
|
||||
await writeFile(`/db/games/${game.id}`, JSON.stringify(reducedGame, null, 2))
|
||||
.catch((error) => {
|
||||
console.error(`${session.id} Unable to write to games/${game.id}`);
|
||||
console.error(`Unable to write to /db/games/${game.id}`);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
@ -3886,7 +3887,7 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
|
||||
/* Check for a game in the Winner state with no more connections
|
||||
* and remove it */
|
||||
if (game.state === 'winner' || game.state === 'lobby') {
|
||||
if (game.state === 'winner') {
|
||||
let dead = true;
|
||||
for (let id in game.sessions) {
|
||||
if (game.sessions[id].live && game.sessions[id].name) {
|
||||
@ -3910,9 +3911,9 @@ router.ws("/ws/:id", async (ws, req) => {
|
||||
delete audio[id];
|
||||
delete games[id];
|
||||
try {
|
||||
fs.unlinkSync(`games/${id}`);
|
||||
fs.unlinkSync(`/db/games/${id}`);
|
||||
} catch (error) {
|
||||
console.error(`${session.id}: Unable to remove games/${id}`);
|
||||
console.error(`${session.id}: Unable to remove /db/games/${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4655,7 +4656,7 @@ const createGame = (id) => {
|
||||
id = randomWords(4).join('-');
|
||||
try {
|
||||
/* If file can be read, it already exists so look for a new name */
|
||||
accessSync(`games/${id}`, fs.F_OK);
|
||||
accessSync(`/db/games/${id}`, fs.F_OK);
|
||||
id = '';
|
||||
} catch (error) {
|
||||
break;
|
||||
|
116
server/src/app.ts
Normal file
116
server/src/app.ts
Normal file
@ -0,0 +1,116 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
process.env.TZ = "Etc/GMT";
|
||||
|
||||
console.log("Loading ketr.ketran");
|
||||
|
||||
const express = require("express");
|
||||
const bodyParser = require("body-parser");
|
||||
const config = require("config");
|
||||
const session = require('express-session');
|
||||
const basePath = require("../basepath");
|
||||
const cookieParser = require("cookie-parser");
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
const server = require("http").createServer(app);
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
const ws = require('express-ws')(app, server);
|
||||
|
||||
require("../console-line.js"); /* Monkey-patch console.log with line numbers */
|
||||
|
||||
const frontendPath = config.get("frontendPath").replace(/\/$/, "") + "/",
|
||||
serverConfig = config.get("server");
|
||||
|
||||
console.log("Hosting server from: " + basePath);
|
||||
|
||||
let userDB: any, gameDB: any;
|
||||
|
||||
app.use(bodyParser.json());
|
||||
|
||||
/* App is behind an nginx proxy which we trust, so use the remote address
|
||||
* set in the headers */
|
||||
app.set("trust proxy", true);
|
||||
|
||||
app.set("basePath", basePath);
|
||||
app.use(basePath, require("../routes/basepath.js"));
|
||||
|
||||
/* Handle static files first so excessive logging doesn't occur */
|
||||
app.use(basePath, express.static(frontendPath, { index: false }));
|
||||
|
||||
const index = require("../routes/index");
|
||||
|
||||
if (config.has("admin")) {
|
||||
const admin = config.get("admin");
|
||||
app.set("admin", admin);
|
||||
}
|
||||
|
||||
/* Allow loading of the app w/out being logged in */
|
||||
app.use(basePath, index);
|
||||
|
||||
/* /games loads the default index */
|
||||
app.use(basePath + "games", index);
|
||||
|
||||
app.use(function(err: any, req: Request, res: Response, next: NextFunction) {
|
||||
console.error(err.message);
|
||||
res.status(err.status || 500).json({
|
||||
message: err.message,
|
||||
error: {}
|
||||
});
|
||||
});
|
||||
|
||||
app.use(`${basePath}api/v1/games`, require("../routes/games"));
|
||||
|
||||
/* Declare the "catch all" index route last; the final route is a 404 dynamic router */
|
||||
app.use(basePath, index);
|
||||
|
||||
/**
|
||||
* Create HTTP server and listen for new connections
|
||||
*/
|
||||
app.set("port", serverConfig.port);
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log("Gracefully shutting down from SIGINT (Ctrl-C) in 2 seconds");
|
||||
setTimeout(() => process.exit(-1), 2000);
|
||||
server.close(() => process.exit(1));
|
||||
});
|
||||
|
||||
require("../db/games").then(function(db: any) {
|
||||
gameDB = db;
|
||||
}).then(function() {
|
||||
return require("../db/users").then(function(db: any) {
|
||||
userDB = db;
|
||||
});
|
||||
}).then(function() {
|
||||
console.log("DB connected. Opening server.");
|
||||
server.listen(serverConfig.port, () => {
|
||||
console.log(`http/ws server listening on ${serverConfig.port}`);
|
||||
});
|
||||
}).catch(function(error: any) {
|
||||
console.error(error);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
server.on("error", function(error: any) {
|
||||
if (error.syscall !== "listen") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case "EACCES":
|
||||
console.error(serverConfig.port + " requires elevated privileges");
|
||||
process.exit(1);
|
||||
break;
|
||||
case "EADDRINUSE":
|
||||
console.error(serverConfig.port + " is already in use");
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { app, server };
|
11
server/tests/app.test.ts
Normal file
11
server/tests/app.test.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import request from 'supertest';
|
||||
const { app } = require('../src/app');
|
||||
|
||||
describe('Server Routes', () => {
|
||||
it('should respond to GET /', async () => {
|
||||
const response = await request(app).get('/');
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
// Add more tests as needed
|
||||
});
|
19
server/tsconfig.json
Normal file
19
server/tsconfig.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "CommonJS",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"strict": false,
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "Node",
|
||||
"resolveJsonModule": true,
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user