diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..c625e46b --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +FROM mcr.microsoft.com/devcontainers/base:debian + +RUN apt-get update && apt-get install -y curl + +# install nvm +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash + +RUN export NVM_DIR="$HOME/.nvm" \ + [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" \ + [ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" + +# install node +RUN nvm install +RUN nvm use + +# install corepack 0.10 only one compatible with node 12 +RUN npm install -g corepack@0.10.0 serve@13.0.4 +RUN corepack prepare yarn@2.4.1 --activate + +# Next steps: +# - yarn install +# - yarn foreach-build-all diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..5d6cd2c8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/alpine +{ + "name": "Alpine", + // More info: https://containers.dev/guide/dockerfile + "build": { + // Path is relative to the devcontainer.json file. + "dockerfile": "Dockerfile" + }, + "features": { + // "ghcr.io/devcontainers/features/node:1": { + // "nodeGypDependencies": true, + // "version": "12", + // "pnpmVersion": "none", + // "nvmVersion": "latest" + // } + } + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + // Configure tool-specific properties. + // "customizations": {}, + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..05dbe874 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +.dockerignore +.git +.gitignore +.idea +.vscode +**/.DS_Store +**/node_modules +packages/*/dist +packages/*/lib +packages/*/build +**/tmp +**/.angular +**/.venv +**/.storybook/ +**/coverage/ +**/.env.local +**/.env.test +**/.eslintcache +**/*.log diff --git a/.vscode/settings.json b/.vscode/settings.json index 181d90f2..5e93a9b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,5 +13,6 @@ "eslint.nodePath": ".yarn/sdks", "typescript.tsdk": ".yarn/sdks/typescript/lib", "typescript.enablePromptUseWorkspaceTsdk": true, - "typescript.preferences.includePackageJsonAutoImports": "on" -} + "typescript.preferences.includePackageJsonAutoImports": "on", + "js/ts.tsdk.path": ".yarn/sdks/typescript/lib" +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0420dbf1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,64 @@ +# monolith: backend & frontend +FROM alpine:3.23 AS monolith + +LABEL org.opencontainers.image.title="Timeflies" \ + org.opencontainers.image.description="Web multiplayer game - tactical-RPG." \ + org.opencontainers.image.url="https://github.com/Chnapy/timeflies" \ + org.opencontainers.image.source="https://github.com/Chnapy/timeflies" \ + org.opencontainers.image.version="0.0.1" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.vendor="Timeflies" \ + org.opencontainers.image.authors="Richard Haddad" \ + org.opencontainers.image.base.name="alpine:3.23" + +RUN apk add --no-cache \ + nginx \ + supervisor \ + curl tar xz \ + && curl -fsSL https://unofficial-builds.nodejs.org/download/release/v12.22.12/node-v12.22.12-linux-x64-musl.tar.xz | \ + tar -xJ -C /usr/local --strip-components=1 \ + && apk del curl tar xz \ + && rm -rf /var/cache/apk/* + +WORKDIR /app + +RUN node --version && npm --version + +# install corepack 0.10 only one compatible with node 12 +RUN npm install -g corepack@0.10.0 serve@13.0.4 +RUN corepack prepare yarn@2.4.1 --activate + +COPY package.json yarn.lock .yarnrc .yarnrc.yml ./ +COPY ./.yarn/releases ./.yarn/releases +COPY ./.yarn/plugins ./.yarn/plugins +COPY ./.yarn/sdks ./.yarn/sdks +COPY ./packages ./packages + +RUN yarn -v && yarn install + +COPY babel.config.js tsconfig.json ./ + +RUN yarn workspace @timeflies/backend p:build-recursive + +RUN yarn workspace @timeflies/frontend p:build-recursive + +# should be defined before frontend final build +ENV REACT_APP_SERVER_URL=/api +ENV REACT_APP_WS_URL=/ws + +RUN yarn workspace @timeflies/frontend build + +# setup logs folders +RUN mkdir -p /var/log/supervisord /var/log/nginx /var/run/nginx \ + && chown -R 755 /var/log/nginx /var/run/nginx \ + && chmod -R 755 /var/log/supervisord + +ENV PORT=40510 +ENV HOST_URL=/api + +COPY nginx.conf /etc/nginx/nginx.conf +COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +EXPOSE 3000 + +CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/README.md b/README.md index f60e7dcd..7afd6297 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,15 @@

Timeflies

-
- PLAY -
+ + +> Project aborted since 2021 Timeflies is a web multiplayer tactical-RPG. This game can be played on any modern browser (= chrome or firefox please). -![Code quality](https://www.code-inspector.com/project/23679/score/svg) -[![Netlify Status](https://api.netlify.com/api/v1/badges/249b0bb9-e394-4f23-a7d0-eafbc965b744/deploy-status)](https://app.netlify.com/sites/timeflies-game/deploys) -![Heroku](https://pyheroku-badge.herokuapp.com/?app=timeflies-game) - ## Bulk features - :musical_note: Wonderful sounds & musics diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..84a55e00 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,47 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 3000; + + # static files (SPA fallback) + location / { + root /app/packages/frontend/build; + index index.html; + try_files $uri $uri/ /index.html; + } + + # proxy backend API calls + location /api/ { + proxy_pass http://localhost:40510/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection keep-alive; + 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; + proxy_cache_bypass $http_upgrade; + } + + # proxy backend websocket calls + location /ws { + proxy_pass http://localhost:40510; + 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; + + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 70438b90..10db6fcf 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -5,7 +5,7 @@ "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { - "start": "PORT=40510 HOST_URL=https://localhost:40510 JWT_PRIVATE_KEY=foo ts-node src/index.ts", + "start": "PORT=40510 HOST_URL=http://localhost:40510 ts-node src/index.ts", "build-all": "yarn p:build-recursive", "serve": "yarn node lib/index.js" }, diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index e25fe18b..bdbc6b83 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -5,30 +5,21 @@ import { json, urlencoded } from 'body-parser'; import cors from "cors"; import express from 'express'; import http from 'http'; -import https from 'https'; import WebSocket from 'ws'; import { config } from './main/config'; import { createGlobalEntities } from './main/global-entities'; import { onAllServicesSocketConnect } from './services/services'; import { getEnv } from './utils/env'; -import { readFileSync } from 'fs'; const port = Number(getEnv('PORT')); -const isLocalhost = getEnv('HOST_URL').includes('://localhost'); - // Express const app = express(); -// TODO do not hardcode -const origin = isLocalhost - ? [ 'https://localhost:3000' ] - : [ 'https://timeflies.fr', 'https://timeflies-game.herokuapp.com' ]; - app.use( cors({ - origin, + origin: '*', allowedHeaders: '*' }), urlencoded({ extended: false }), @@ -41,14 +32,7 @@ const globalEntities = createGlobalEntities(); app.post(authEndpoint, globalEntities.services.authService.httpAuthRoute); -const server = isLocalhost - // https in localhost to simulate Heroku https - ? https.createServer({ - key: readFileSync('../../localhost-key.pem'), - cert: readFileSync('../../localhost.pem') - }, app) - // Heroku do not need https server (it causes errors) - : http.createServer(app); +const server = http.createServer(app); // Websocket diff --git a/packages/backend/src/services/auth/auth-service.ts b/packages/backend/src/services/auth/auth-service.ts index ed0fccd2..e596a1f0 100644 --- a/packages/backend/src/services/auth/auth-service.ts +++ b/packages/backend/src/services/auth/auth-service.ts @@ -10,8 +10,6 @@ import { PlayerCredentialsTimed } from '../../main/global-entities'; import { getEnv } from '../../utils/env'; import { Service } from '../service'; -const privateKey = getEnv('JWT_PRIVATE_KEY'); - export class AuthService extends Service { protected afterSocketConnect = () => { }; @@ -72,7 +70,7 @@ export class AuthService extends Service { playerName }; - const token = JWT.sign(tokenPayload, privateKey, { algorithm: 'HS256' }); + const token = JWT.sign(tokenPayload, 'none', { algorithm: 'HS256' }); const playerCredentials: PlayerCredentials = { playerId, @@ -100,9 +98,9 @@ export class AuthService extends Service { } const token = new URLSearchParams( - url.startsWith('/?') // localhost - ? url.substring(2) - : new URL(url).search + url.startsWith('http') + ? new URL(url).search + : new URL(url, 'http://localhost').search ).get('token')!; const socketQueryParams: SocketQueryParams = { token }; diff --git a/packages/backend/src/setup-tests.ts b/packages/backend/src/setup-tests.ts index 94199bef..f7502419 100644 --- a/packages/backend/src/setup-tests.ts +++ b/packages/backend/src/setup-tests.ts @@ -3,5 +3,4 @@ import { applySetupTests } from '@timeflies/devtools'; applySetupTests(); process.env.PORT = '1234'; -process.env.HOST_URL = 'https://host.com'; -process.env.JWT_PRIVATE_KEY = 'foo'; +process.env.HOST_URL = 'http://host.com'; diff --git a/packages/backend/src/utils/asset-url.test.ts b/packages/backend/src/utils/asset-url.test.ts index a7091b98..0c190c3a 100644 --- a/packages/backend/src/utils/asset-url.test.ts +++ b/packages/backend/src/utils/asset-url.test.ts @@ -8,7 +8,7 @@ describe('asset url', () => { }); it('to frontend', () => { - expect(assetUrl.toFrontend('foo/bar')).toEqual('https://host.com/static/foo/bar'); - expect(assetUrl.toFrontend('/foo/bar')).toEqual('https://host.com/static/foo/bar'); + expect(assetUrl.toFrontend('foo/bar')).toEqual('http://host.com/static/foo/bar'); + expect(assetUrl.toFrontend('/foo/bar')).toEqual('http://host.com/static/foo/bar'); }); }); diff --git a/packages/backend/src/utils/env.ts b/packages/backend/src/utils/env.ts index 45330955..942e9b7a 100644 --- a/packages/backend/src/utils/env.ts +++ b/packages/backend/src/utils/env.ts @@ -1,5 +1,5 @@ -const requiredKeys = [ 'PORT', 'HOST_URL', 'JWT_PRIVATE_KEY' ] as const; +const requiredKeys = [ 'PORT', 'HOST_URL' ] as const; console.table(requiredKeys.reduce>((acc, key) => { acc[ key ] = process.env[ key ]; diff --git a/packages/frontend/.env b/packages/frontend/.env index cdc177b8..2002bfaf 100644 --- a/packages/frontend/.env +++ b/packages/frontend/.env @@ -3,4 +3,4 @@ REACT_APP_TITLE=Timeflies | Multiplayer tactical-RPG game REACT_APP_DESCRIPTION=Timeflies is a free multiplayer tactical-RPG game, with a time-based gameplay. REACT_APP_AUTHOR=Richard Haddad REACT_APP_PREVIEW_IMG=https://repository-images.githubusercontent.com/238224907/2e8aad0f-7446-4cd2-a628-577f86f47174 -REACT_APP_URL=https://timeflies.fr +REACT_APP_URL=https://github.com/Chnapy/timeflies diff --git a/packages/frontend/.env.local b/packages/frontend/.env.local index 4042c2ad..e3b7933c 100644 --- a/packages/frontend/.env.local +++ b/packages/frontend/.env.local @@ -1 +1,2 @@ -REACT_APP_SERVER_URL=https://localhost:40510 \ No newline at end of file +REACT_APP_SERVER_URL=http://localhost:40510 +REACT_APP_WS_URL=http://localhost:40510 diff --git a/packages/frontend/.env.test b/packages/frontend/.env.test index f21e56e8..375a98bf 100644 --- a/packages/frontend/.env.test +++ b/packages/frontend/.env.test @@ -1 +1,2 @@ -REACT_APP_SERVER_URL=foo \ No newline at end of file +REACT_APP_SERVER_URL=foo +REACT_APP_WS_URL=foo \ No newline at end of file diff --git a/packages/frontend/mock-server/index.ts b/packages/frontend/mock-server/index.ts index 096a4e4a..f3ada0f5 100644 --- a/packages/frontend/mock-server/index.ts +++ b/packages/frontend/mock-server/index.ts @@ -17,13 +17,13 @@ app.use('/public', express.static('mock-server/public')); const server = http.createServer(app); const port = 40510; -const wss = new WebSocketServer({ server }); +const ws = new WebSocketServer({ server }); console.log('Mock server started'); server.listen(port, () => { console.log('server listening address', server.address()); - console.log('ws listening address', wss.address()); + console.log('ws listening address', ws.address()); }); const turnsOrder = [ 'c2', 'c1', 'c3' ]; @@ -173,7 +173,7 @@ const battleLoadData: BattleLoadData = { const sendAll = action => { console.log('sendAll: %s', JSON.stringify(action)); - wss.clients.forEach(ws => { + ws.clients.forEach(ws => { ws.send(JSON.stringify([ action ])); }); }; @@ -194,7 +194,7 @@ const sendAllNextTurnInfos = () => setTimeout(() => BattleTurnStartMessage(cycleEngine.getNextTurnInfos()) ), 1000); -wss.on('connection', function (ws) { +ws.on('connection', function (ws) { const startTime = Date.now() + 5000; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index a60576cf..57b4c899 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -5,7 +5,7 @@ "main": "lib/index.js", "types": "lib/index.d.ts", "scripts": { - "start": "yarn copy-assets && HTTPS=true SSL_CRT_FILE=../../localhost.pem SSL_KEY_FILE=../../localhost-key.pem react-scripts start", + "start": "yarn copy-assets && react-scripts start", "build": "yarn copy-assets && react-scripts build", "build-all": "yarn p:build-recursive && yarn build", "test": "react-scripts test", diff --git a/packages/frontend/src/connected-socket/hooks/use-connected-socket-helper.ts b/packages/frontend/src/connected-socket/hooks/use-connected-socket-helper.ts index 39e2d594..f1117362 100644 --- a/packages/frontend/src/connected-socket/hooks/use-connected-socket-helper.ts +++ b/packages/frontend/src/connected-socket/hooks/use-connected-socket-helper.ts @@ -7,7 +7,12 @@ import { useDispatchMessageErrorIfAny } from '../../error-list/hooks/use-dispatc import { CredentialsLoginAction } from '../../login-page/store/credentials-actions'; import { useGameSelector } from '../../store/hooks/use-game-selector'; -const createSocketHelper = getSocketHelperCreator(getEnv('REACT_APP_SERVER_URL')); +const socketUrl = new URL( + getEnv('REACT_APP_WS_URL'), + window.location.href +).href; + +const createSocketHelper = getSocketHelperCreator(socketUrl); export const useConnectedSocketHelper = () => { const credentialsToken = useGameSelector(state => state.credentials?.token); diff --git a/packages/frontend/src/env.ts b/packages/frontend/src/env.ts index 659e3c58..48d28a81 100644 --- a/packages/frontend/src/env.ts +++ b/packages/frontend/src/env.ts @@ -1,5 +1,5 @@ -const requiredKeys = [ 'REACT_APP_SERVER_URL' ] as const; +const requiredKeys = [ 'REACT_APP_SERVER_URL', 'REACT_APP_WS_URL' ] as const; console.table(requiredKeys.reduce>((acc, key) => { acc[ key ] = process.env[ key ]; diff --git a/packages/frontend/src/login-page/hooks/use-login-form-state.ts b/packages/frontend/src/login-page/hooks/use-login-form-state.ts index 7edb24d4..0ba25d53 100644 --- a/packages/frontend/src/login-page/hooks/use-login-form-state.ts +++ b/packages/frontend/src/login-page/hooks/use-login-form-state.ts @@ -9,8 +9,8 @@ import { ErrorListAddAction } from '../../error-list/store/error-list-actions'; import { CredentialsLoginAction } from '../store/credentials-actions'; const authUrl = new URL( - authEndpoint, - getEnv('REACT_APP_SERVER_URL') + getEnv('REACT_APP_SERVER_URL') + authEndpoint, + window.location.href ).href; const playerNameInputName = 'playerName'; diff --git a/packages/socket-client/src/socket/create-socket-helper.test.ts b/packages/socket-client/src/socket/create-socket-helper.test.ts index 729587c3..e663e9fe 100644 --- a/packages/socket-client/src/socket/create-socket-helper.test.ts +++ b/packages/socket-client/src/socket/create-socket-helper.test.ts @@ -30,9 +30,9 @@ describe('# create socket helper', () => { it('connect to correct endpoint', () => { mockWebSocket(); - const socketHelper = getSocketHelperCreator('https://myendpoint.foo.bar/toto')('my_token'); + const socketHelper = getSocketHelperCreator('http://myendpoint.foo.bar/toto')('my_token'); - expect(socketHelper.url).toEqual('wss://myendpoint.foo.bar/toto?token=my_token'); + expect(socketHelper.url).toEqual('ws://myendpoint.foo.bar/toto?token=my_token'); }); describe('hearbeat timeout loop', () => { diff --git a/packages/socket-client/src/socket/create-socket-helper.ts b/packages/socket-client/src/socket/create-socket-helper.ts index d23faa47..4a1e454f 100644 --- a/packages/socket-client/src/socket/create-socket-helper.ts +++ b/packages/socket-client/src/socket/create-socket-helper.ts @@ -2,7 +2,7 @@ import { extractMessagesFromEvent, heartbeatMessageValue, Message } from '@timef export type SocketHelper = ReturnType>; -const createEndpoint = (protocol: 'https' | 'wss', url: string) => { +const createEndpoint = (protocol: 'http' | 'ws', url: string) => { const startIndex = url.indexOf('://'); if (startIndex !== -1) { @@ -14,7 +14,7 @@ const createEndpoint = (protocol: 'https' | 'wss', url: string) => { const createAuthenticatedEndpoint = (token: string, serverUrl: string): string => { - const baseEndpoint = createEndpoint('wss', serverUrl); + const baseEndpoint = createEndpoint('ws', serverUrl); const url = new URL(baseEndpoint); url.searchParams.set('token', token); diff --git a/supervisord.conf b/supervisord.conf new file mode 100644 index 00000000..38a8aaf7 --- /dev/null +++ b/supervisord.conf @@ -0,0 +1,36 @@ +[supervisord] +nodaemon=true +user=root +logfile=/dev/fd/1 +logfile_maxbytes=0 +pidfile=/var/run/supervisord.pid + +[program:backend] +command=yarn node /app/packages/backend/lib/index.js +directory=/app/packages/backend +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +# stderr_logfile=/dev/fd/2 +redirect_stderr=true +# environment=DOTNET_SYSTEM_GLOBALIZATION_INVARIANT="0",ASPNETCORE_ENVIRONMENT="Production",ASPNETCORE_URLS="http://+:5000" + +[program:nginx] +command=nginx -g "daemon off;" +autostart=true +autorestart=true +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +# stderr_logfile=/dev/fd/2 +redirect_stderr=true + +[program:healthcheck] # global healthcheck +command=curl -f http://localhost:3000 || exit 1 +autostart=false +autorestart=true +startsecs=1 +stdout_logfile=/dev/fd/1 +stdout_logfile_maxbytes=0 +# stderr_logfile=/dev/fd/2 +redirect_stderr=true