Updated docker and server configs
This commit is contained in:
parent
eb39a175df
commit
b553cdc656
@ -1,5 +1,7 @@
|
|||||||
*
|
*
|
||||||
!server
|
!server/
|
||||||
server/node_modules
|
!client/
|
||||||
|
server/node_modules/
|
||||||
|
client/node_modules/
|
||||||
!Dockerfile
|
!Dockerfile
|
||||||
!.env
|
!.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
|
!package.json
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
22
Dockerfile
22
Dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM ubuntu:jammy
|
FROM ubuntu:noble
|
||||||
|
|
||||||
RUN apt-get -q update \
|
RUN apt-get -q update \
|
||||||
&& DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \
|
&& 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 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
|
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 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 \
|
RUN apt-get -q update \
|
||||||
@ -25,12 +25,26 @@ RUN apt-get -q update \
|
|||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
|
&& rm -rf /var/lib/apt/lists/{apt,dpkg,cache,log}
|
||||||
|
|
||||||
COPY /server /server
|
COPY server /server
|
||||||
WORKDIR /server
|
WORKDIR /server
|
||||||
RUN npm install -s sqlite3
|
RUN npm install -s sqlite3
|
||||||
RUN npm install
|
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 /Dockerfile /Dockerfile
|
||||||
COPY /.env /.env
|
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
|
# Peddlers of Ketran
|
||||||
|
|
||||||
This project consists of both the front-end and back-end game
|
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.
|
||||||
API server.
|
|
||||||
|
|
||||||
The front-end is launched from the 'client' directory in development mode via 'npm start'. In production, you build
|
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.
|
||||||
it via 'npm build' and deploy the public front-end.
|
|
||||||
|
|
||||||
The back-end is launched out of the 'server' directory via 'npm start' and will
|
## Development
|
||||||
bind to the default port 8930.
|
|
||||||
|
|
||||||
If you change the default port of the REST API server, you will need to change
|
For live development with hot reloading, React DevTools support, and TypeScript compilation on-the-fly.
|
||||||
client/package.json's "proxy" value to reflect the new port change.
|
|
||||||
|
|
||||||
NOTE:
|
### Prerequisites
|
||||||
|
|
||||||
Board.js currently hard codes assetsPath and gamesPath to be absolute as the
|
- Docker
|
||||||
dynamic router and resource / asset loading isn't working correctly.
|
- Docker Compose
|
||||||
|
|
||||||
## Building
|
### Running the Server (Backend)
|
||||||
|
|
||||||
### Native
|
The server uses TypeScript with hot reloading via `ts-node-dev`.
|
||||||
|
|
||||||
#### Prerequisites
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt-get install -y nodejs npm python
|
docker compose -f docker-compose.dev.yml up
|
||||||
sudo -E npm install --global npm@latest
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 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
|
```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
|
```plantuml
|
||||||
skinparam componentStyle rectangle
|
skinparam componentStyle rectangle
|
||||||
@ -48,99 +124,40 @@ package "Game" as game {
|
|||||||
component Players as players
|
component Players as players
|
||||||
}
|
}
|
||||||
|
|
||||||
server <-> resource : serves to client
|
server <-> resource : serves static files to client
|
||||||
client <-> server
|
client <-> server : API calls
|
||||||
player <-> client
|
player <-> client
|
||||||
players -r-> player
|
players -r-> player
|
||||||
server -> game
|
server -> game
|
||||||
```
|
```
|
||||||
|
|
||||||
# Ketr.Ketran REST API
|
## API Documentation
|
||||||
|
|
||||||
## POST /api/v1/game
|
### POST /api/v1/game
|
||||||
|
|
||||||
### Request
|
#### Request
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{}
|
{}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Response
|
#### Response
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
gameId: id
|
"gameId": "id",
|
||||||
gameState: {
|
"gameState": {
|
||||||
tiles: []
|
"tiles": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
# Configuring / installing
|
## Configuration
|
||||||
|
|
||||||
## Build
|
Add security tokens in `server/config/local.json`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone git.ketrenos.com:jketreno/peddlers-of-ketran.git
|
cat << EOF > server/config/local.json
|
||||||
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
|
|
||||||
{
|
{
|
||||||
"tokens": [ {
|
"tokens": [ {
|
||||||
"$(whoami)": "$(< /dev/urandom tr -dc _A-Z-a-z-0-9 | head -c${1:-32};echo;)"
|
"$(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
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
## Launch
|
## Testing
|
||||||
|
|
||||||
|
### Create New Game
|
||||||
```bash
|
```bash
|
||||||
sudo systemctl start ketr.ketran
|
curl -X POST http://localhost:8930/api/v1/games/
|
||||||
```
|
```
|
||||||
|
|
||||||
## To test
|
### Get Game Status
|
||||||
|
|
||||||
### New game
|
|
||||||
```bash
|
```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
|
- **Lobby**: Players register, choose colors, roll for position, set ready
|
||||||
curl -k -s -X GET http://localhost:8930/ketr.ketran/api/v1/games/:id
|
- **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
|
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/).
|
||||||
|
|
||||||
|
|
||||||
* 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
|
|
||||||
|
|
||||||
*
|
|
||||||
|
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(/\/+$/, "") + "/";
|
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
|
||||||
if (basePath == "//") {
|
if (basePath == "//") {
|
||||||
basePath = "/";
|
basePath = "/";
|
||||||
|
@ -1 +1,3 @@
|
|||||||
{}
|
{
|
||||||
|
"frontendPath": "/client/build"
|
||||||
|
}
|
||||||
|
@ -3,7 +3,12 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"scripts": {
|
"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>",
|
"author": "James Ketrenos <james_settlers@ketrenos.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -30,6 +35,19 @@
|
|||||||
"typeface-roboto": "0.0.75",
|
"typeface-roboto": "0.0.75",
|
||||||
"ws": "^8.5.0"
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git@git.ketrenos.com:jketreno/peddlers-of-ketran"
|
"url": "git@git.ketrenos.com:jketreno/peddlers-of-ketran"
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
const express = require("express"),
|
const express = require("express"),
|
||||||
router = express.Router(),
|
router = express.Router(),
|
||||||
crypto = require("crypto"),
|
crypto = require("crypto")
|
||||||
{ readFile, writeFile } = require("fs").promises,
|
const { readFile, writeFile, mkdir } = require("fs").promises,
|
||||||
fs = require("fs"),
|
fs = require("fs"),
|
||||||
accessSync = fs.accessSync,
|
accessSync = fs.accessSync,
|
||||||
randomWords = require("random-words"),
|
randomWords = require("random-words"),
|
||||||
@ -619,7 +619,7 @@ const loadGame = async (id) => {
|
|||||||
return games[id];
|
return games[id];
|
||||||
}
|
}
|
||||||
|
|
||||||
let game = await readFile(`games/${id}`)
|
let game = await readFile(`/db/games/${id}`)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
@ -627,20 +627,20 @@ const loadGame = async (id) => {
|
|||||||
if (game) {
|
if (game) {
|
||||||
try {
|
try {
|
||||||
game = JSON.parse(game);
|
game = JSON.parse(game);
|
||||||
console.log(`${info}: Creating backup of games/${id}`);
|
console.log(`${info}: Creating backup of /db/games/${id}`);
|
||||||
await writeFile(`games/${id}.bk`, JSON.stringify(game));
|
await writeFile(`/db/games/${id}.bk`, JSON.stringify(game));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Load or parse error from games/${id}:`, error);
|
console.log(`Load or parse error from /db/games/${id}:`, error);
|
||||||
console.log(`Attempting to load backup from games/${id}.bk`);
|
console.log(`Attempting to load backup from /db/games/${id}.bk`);
|
||||||
game = await readFile(`games/${id}.bk`)
|
game = await readFile(`/db/games/${id}.bk`)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.error(error, game);
|
console.error(error, game);
|
||||||
});
|
});
|
||||||
if (game) {
|
if (game) {
|
||||||
try {
|
try {
|
||||||
game = JSON.parse(game);
|
game = JSON.parse(game);
|
||||||
console.log(`Saving backup to games/${id}`);
|
console.log(`Saving backup to /db/games/${id}`);
|
||||||
await writeFile(`games/${id}`, JSON.stringify(game, null, 2));
|
await writeFile(`/db/games/${id}`, JSON.stringify(game, null, 2));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
game = null;
|
game = null;
|
||||||
@ -3448,15 +3448,16 @@ const saveGame = async (game) => {
|
|||||||
/* Save per turn while debugging... */
|
/* Save per turn while debugging... */
|
||||||
game.step = game.step ? game.step : 0;
|
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) => {
|
.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);
|
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) => {
|
.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);
|
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
|
/* Check for a game in the Winner state with no more connections
|
||||||
* and remove it */
|
* and remove it */
|
||||||
if (game.state === 'winner' || game.state === 'lobby') {
|
if (game.state === 'winner') {
|
||||||
let dead = true;
|
let dead = true;
|
||||||
for (let id in game.sessions) {
|
for (let id in game.sessions) {
|
||||||
if (game.sessions[id].live && game.sessions[id].name) {
|
if (game.sessions[id].live && game.sessions[id].name) {
|
||||||
@ -3910,9 +3911,9 @@ router.ws("/ws/:id", async (ws, req) => {
|
|||||||
delete audio[id];
|
delete audio[id];
|
||||||
delete games[id];
|
delete games[id];
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(`games/${id}`);
|
fs.unlinkSync(`/db/games/${id}`);
|
||||||
} catch (error) {
|
} 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('-');
|
id = randomWords(4).join('-');
|
||||||
try {
|
try {
|
||||||
/* If file can be read, it already exists so look for a new name */
|
/* 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 = '';
|
id = '';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
break;
|
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