diff --git a/.dockerignore b/.dockerignore index 4b26b5a70..215cff0ba 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,7 @@ client/node_modules server/node_modules -server/deploy/Dockerfile* +server/Dockerfile.dev +server/Dockerfile.prod server/output docker keys diff --git a/Makefile b/Makefile index c3b48f211..4ab1471b4 100644 --- a/Makefile +++ b/Makefile @@ -1,55 +1,39 @@ -include config.mk - -DB_USER=marsmadness -TEST_DB_NAME=pom_testing DB_DATA_PATH=docker/data DATA_DUMP_PATH=docker/dump LOG_DATA_PATH=docker/logs + DB_PASSWORD_PATH=keys/pom_db_password -REDIS_SETTINGS_PATH=keys/settings.json -ORMCONFIG_PATH=keys/ormconfig.json -SERVER_ENV_TEMPLATE=server/.env.template -SERVER_ENV=server/.env PGPASS_PATH=keys/.pgpass SECRET_KEY_PATH=keys/secret_key -SENTRY_DSN_PATH=keys/sentry_dsn -SENTRY_DSN=$(shell cat $(SENTRY_DSN_PATH)) -MAIL_API_KEY_PATH=keys/mail_api_key -SECRETS=$(MAIL_API_KEY_PATH) $(DB_PASSWORD_PATH) $(ORMCONFIG_PATH) $(PGPASS_PATH) $(SENTRY_DSN_PATH) $(SECRET_KEY_PATH) -SHARED_CONFIG_PATH=shared/src/assets/config.ts -BUILD_ID=$(shell git describe --tags --abbrev=1) -GA_TAG_PATH=keys/ga_tag -GA_TAG=$(shell cat $(GA_TAG_PATH)) +GENERATED_SECRETS=$(DB_PASSWORD_PATH) $(PGPASS_PATH) $(SECRET_KEY_PATH) +EXT_SECRETS=mail_api_key google_client_secret facebook_client_secret -.PHONY: build -build: docker-compose.yml - docker compose pull db redis - docker compose build --pull +ENVREPLACE := deploy/scripts/envreplace +DEPLOY_CONF_DIR=deploy/conf +ENV_TEMPLATE=${DEPLOY_CONF_DIR}/.env.template +DYNAMIC_SETTINGS_TEMPLATE=${DEPLOY_CONF_DIR}/settings.template.json +DYNAMIC_SETTINGS_PATH=keys/settings.json + +include config.mk +include .env + +.EXPORT_ALL_VARIABLES: + +$(LOG_DATA_PATH): + mkdir -p $(LOG_DATA_PATH) + +$(DB_DATA_PATH): + mkdir -p "$(DB_DATA_PATH)" -.PHONY: browser -browser: - firefox --new-tab --url 'ext+container:name=Bob&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Amanda&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Frank&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Sydney&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Adison&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Bob2&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Amanda2&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Frank2&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Sydney2&url=http://localhost:8081/#/game' \ - --new-tab --url 'ext+container:name=Adison2&url=http://localhost:8081/#/game' - -.PHONY: browser-staging -browser-staging: - firefox --new-tab --url 'ext+container:name=Bob&url=http://alpha.portofmars.asu.edu' \ - --new-tab --url 'ext+container:name=Amanda&url=http://alpha.portofmars.asu.edu' \ - --new-tab --url 'ext+container:name=Frank&url=http://alpha.portofmars.asu.edu' \ - --new-tab --url 'ext+container:name=Sydney&url=http://alpha.portofmars.asu.edu' \ - --new-tab --url 'ext+container:name=Adison&url=http://alpha.portofmars.asu.edu' +$(DATA_DUMP_PATH): + mkdir -p $(DATA_DUMP_PATH) keys: mkdir -p keys +$(DYNAMIC_SETTINGS_PATH): $(DYNAMIC_SETTINGS_TEMPLATE) | keys + cp $(DYNAMIC_SETTINGS_TEMPLATE) $(DYNAMIC_SETTINGS_PATH) + $(DB_PASSWORD_PATH): | keys DB_PASSWORD=$$(openssl rand -base64 48); \ TODAY=$$(date +%Y-%m-%d-%H:%M:%S); \ @@ -59,66 +43,56 @@ $(DB_PASSWORD_PATH): | keys fi; \ echo "$${DB_PASSWORD}" > $(DB_PASSWORD_PATH) -$(LOG_DATA_PATH): - mkdir -p $(LOG_DATA_PATH) - -$(DATA_DUMP_PATH): - mkdir -p $(DATA_DUMP_PATH) - -$(REDIS_SETTINGS_PATH): server/deploy/settings.template.json | keys - cp server/deploy/settings.template.json $(REDIS_SETTINGS_PATH) - -$(ORMCONFIG_PATH): server/ormconfig.template.json $(DB_PASSWORD_PATH) - DB_PASSWORD=$$(cat $(DB_PASSWORD_PATH)); \ - sed "s|DB_PASSWORD|$$DB_PASSWORD|g" server/ormconfig.template.json > $(ORMCONFIG_PATH) - -$(SERVER_ENV): $(SERVER_ENV_TEMPLATE) $(SECRETS) - POM_BASE_URL=${POM_BASE_URL} \ - envsubst < $(SERVER_ENV_TEMPLATE) > $(SERVER_ENV) - -$(PGPASS_PATH): $(DB_PASSWORD_PATH) server/deploy/pgpass.template | keys - DB_PASSWORD=$$(cat $(DB_PASSWORD_PATH)); \ - sed "s|DB_PASSWORD|$$DB_PASSWORD|g" server/deploy/pgpass.template > $(PGPASS_PATH) +$(PGPASS_PATH): $(DB_PASSWORD_PATH) | keys + echo "${DB_HOST}:5432:*:${DB_USER}:$$(cat $(DB_PASSWORD_PATH))" > $(PGPASS_PATH) chmod 0600 $(PGPASS_PATH) -$(MAIL_API_KEY_PATH): | keys - touch "$(MAIL_API_KEY_PATH)" - -$(SENTRY_DSN_PATH): | keys - touch "$(SENTRY_DSN_PATH)" - -$(GA_TAG_PATH): | keys - touch "$(GA_TAG_PATH)" - -$(DB_DATA_PATH): - mkdir -p "$(DB_DATA_PATH)" - -.PHONY: secrets -secrets: $(SECRETS) - $(SECRET_KEY_PATH): | keys SECRET_KEY=$$(openssl rand -base64 48); \ echo $${SECRET_KEY} > $(SECRET_KEY_PATH) -.PHONY: settings -settings: $(SENTRY_DSN_PATH) $(SECRET_KEY_PATH) | keys - echo 'export const BUILD_ID = "${BUILD_ID}";' > $(SHARED_CONFIG_PATH) - echo 'export const SENTRY_DSN = "${SENTRY_DSN}";' >> $(SHARED_CONFIG_PATH) - echo 'export const GA_TAG = "${GA_TAG}";' >> $(SHARED_CONFIG_PATH) +.PHONY: secrets +secrets: keys $(GENERATED_SECRETS) + for secret_path in $(EXT_SECRETS); do \ + touch keys/$$secret_path; \ + done + +.env: $(ENV_TEMPLATE) + if [ ! -f .env ]; then \ + cp $(ENV_TEMPLATE) .env; \ + fi + +.PHONY: release-version +release-version: .env + $(ENVREPLACE) SHARED_RELEASE_VERSION $$(git describe --tags --abbrev=1) .env + +docker-compose.yml: base.yml $(DEPLOY_ENVIRONMENT).yml config.mk $(DB_DATA_PATH) $(DATA_DUMP_PATH) $(LOG_DATA_PATH) $(DYNAMIC_SETTINGS_PATH) secrets $(PGPASS_PATH) release-version + case "$(DEPLOY_ENVIRONMENT)" in \ + dev|staging|prod) docker compose -f base.yml -f "$(DEPLOY_ENVIRONMENT).yml" config > docker-compose.yml;; \ + *) echo "invalid environment. must be either dev, staging or prod" 1>&2; exit 1;; \ + esac + +.PHONY: build +build: docker-compose.yml + docker compose pull db redis + docker compose build --pull + +.PHONY: deploy +deploy: build + docker compose up -d +.PHONY: buildprod +buildprod: docker-compose.yml + docker compose run --rm client npm run build + docker compose run --rm server npm run build +.PHONY: initialize initialize: build docker compose run --rm server npm run initdb -docker-compose.yml: base.yml $(ENVIR).yml config.mk $(DB_DATA_PATH) $(DATA_DUMP_PATH) $(LOG_DATA_PATH) $(REDIS_SETTINGS_PATH) $(ORMCONFIG_PATH) $(NUXT_ORMCONFIG_PATH) $(PGPASS_PATH) $(SERVER_ENV) settings - case "$(ENVIR)" in \ - dev|staging|prod) docker compose -f base.yml -f "$(ENVIR).yml" config > docker-compose.yml;; \ - *) echo "invalid environment. must be either dev, staging or prod" 1>&2; exit 1;; \ - esac - .PHONY: test-setup test-setup: docker-compose.yml - docker compose run --rm server bash -c "dropdb --if-exists -h db -U ${DB_USER} ${TEST_DB_NAME} && createdb -h db -U ${DB_USER} ${TEST_DB_NAME} && npm run typeorm -- schema:sync -c test && npm run load-fixtures -- ./fixtures/sologame -cn test" + docker compose run --rm server bash -c "dropdb --if-exists -h ${DB_HOST} -U ${DB_USER} ${TEST_DB_NAME} && createdb -h db -U ${DB_USER} ${TEST_DB_NAME} && npm run test-setup" .PHONY: test test: test-setup @@ -127,16 +101,7 @@ test: test-setup .PHONY: test-server test-server: test-setup - docker compose run --rm server npm run test $(tests) - -.PHONY: deploy -deploy: build - docker compose up -d - -.PHONY: buildprod -buildprod: docker-compose.yml - docker compose run --rm client npm run build - docker compose run --rm server npm run build + docker compose run --rm server npm run test .PHONY: docker-clean docker-clean: @@ -146,4 +111,5 @@ docker-clean: .PHONY: clean clean: - rm -f server/.env # any other generated resources? SHARED_CONFIG_PATH? + @echo "Backing up generated files to /tmp directory" + mv .env config.mk docker-compose.yml $(shell mktemp -d) diff --git a/base.yml b/base.yml index 880fef79a..dd329ba39 100644 --- a/base.yml +++ b/base.yml @@ -4,6 +4,14 @@ services: context: . restart: always image: port-of-mars/server:dev + secrets: + - pom_db_password + - mail_api_key + - secret_key + - google_client_secret + - facebook_client_secret + env_file: + - .env depends_on: redis: condition: service_started @@ -12,11 +20,9 @@ services: volumes: - ./docker/dump:/dump - ./docker/logs:/var/log/port-of-mars - - ./keys/ormconfig.json:/code/server/ormconfig.json - ./keys/.pgpass:/root/.pgpass - - ./keys:/run/secrets + - ./keys/settings.json:/run/secrets/settings.json - ./scripts:/scripts - - ./server/.env:/code/server/.env - ./.prettierrc:/code/.prettierrc redis: image: redis:7 @@ -28,12 +34,25 @@ services: timeout: 5s retries: 5 image: postgres:12 + secrets: + - pom_db_password restart: always environment: - POSTGRES_USER: marsmadness + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD_FILE: /run/secrets/pom_db_password - POSTGRES_DB: port_of_mars PGDATA: /var/lib/postgresql/data/pgdata volumes: - - ./keys/pom_db_password:/run/secrets/pom_db_password:ro - ./docker/data:/var/lib/postgresql/data/pgdata + +secrets: + pom_db_password: + file: ./keys/pom_db_password + mail_api_key: + file: ./keys/mail_api_key + secret_key: + file: ./keys/secret_key + google_client_secret: + file: ./keys/google_client_secret + facebook_client_secret: + file: ./keys/facebook_client_secret diff --git a/client/src/components/game/Inventory.vue b/client/src/components/game/Inventory.vue index 7fe1eec8f..d7668e244 100644 --- a/client/src/components/game/Inventory.vue +++ b/client/src/components/game/Inventory.vue @@ -46,7 +46,6 @@ import { Vue, Component, Prop } from "vue-property-decorator"; import { Role, RESEARCHER } from "@port-of-mars/shared/types"; import { Investment, Resource, RESOURCES, Phase } from "@port-of-mars/shared/types"; -import { Constants } from "@port-of-mars/shared/settings"; @Component({ components: {}, @@ -96,7 +95,7 @@ export default class Inventory extends Vue { } canInvest(cost: number): boolean { - return cost < Constants.MAXIMUM_COST; + return cost < this.$settings.MAXIMUM_COST; } toggleCosts() { diff --git a/client/src/components/game/phases/investment/InvestmentCard.vue b/client/src/components/game/phases/investment/InvestmentCard.vue index 7e41cacaf..e358a5d4b 100644 --- a/client/src/components/game/phases/investment/InvestmentCard.vue +++ b/client/src/components/game/phases/investment/InvestmentCard.vue @@ -56,7 +56,6 @@ diff --git a/client/src/components/lobby/LobbyRoomList.vue b/client/src/components/lobby/LobbyRoomList.vue index 5b898e5b3..c8b70a4b2 100644 --- a/client/src/components/lobby/LobbyRoomList.vue +++ b/client/src/components/lobby/LobbyRoomList.vue @@ -101,7 +101,6 @@ import { Component, Inject, Vue } from "vue-property-decorator"; import { Client } from "colyseus.js"; import { FREE_PLAY_LOBBY_NAME } from "@port-of-mars/shared/lobby"; -import { Constants } from "@port-of-mars/shared/settings"; import MuteBanWarning from "@port-of-mars/client/components/lobby/MuteBanWarning.vue"; @Component({ @@ -123,10 +122,6 @@ export default class LobbyRoomList extends Vue { refreshingRoomList = false; pollingIntervalId = 0; - get constants() { - return Constants; - } - get isBanned() { return this.$store.state.user.isBanned; } diff --git a/client/src/main.ts b/client/src/main.ts index 0fb91be64..c4fca3ce2 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -7,7 +7,7 @@ import Vuex from "vuex"; import * as Sentry from "@sentry/browser"; import { Vue as VueIntegration } from "@sentry/integrations"; import { Integrations } from "@sentry/tracing"; -import { isStagingOrProduction, Constants } from "@port-of-mars/shared/settings"; +import { settings, isStagingOrProduction, SERVER_URL_WS } from "@port-of-mars/shared/settings"; import { Ajax } from "@port-of-mars/client/plugins/ajax"; import { TypedStore } from "@port-of-mars/client/plugins/tstore"; import { getAssetUrl, SfxManager } from "@port-of-mars/client/util"; @@ -25,7 +25,7 @@ Vue.config.productionTip = false; if (isStagingOrProduction()) { Sentry.init({ - dsn: Constants.SENTRY_DSN, + dsn: import.meta.env.SHARED_SENTRY_DSN, integrations: [new VueIntegration({ Vue, tracing: true }), new Integrations.BrowserTracing()], tracesSampleRate: 1, }); @@ -33,17 +33,24 @@ if (isStagingOrProduction()) { VueGtag, { config: { - id: Constants.GA_TAG, + id: import.meta.env.SHARED_GA_TAG, }, }, router ); } -const $client = new Colyseus.Client(process.env.SERVER_URL_WS || undefined); +const $client = new Colyseus.Client(SERVER_URL_WS || undefined); const $sfx = new SfxManager(); Vue.prototype.$getAssetUrl = getAssetUrl; +Vue.prototype.$settings = settings; +declare module "vue/types/vue" { + interface Vue { + $settings: typeof settings; + $getAssetUrl: typeof getAssetUrl; + } +} new Vue({ router, diff --git a/client/src/plugins/settings.ts b/client/src/plugins/settings.ts new file mode 100644 index 000000000..63d1a27fd --- /dev/null +++ b/client/src/plugins/settings.ts @@ -0,0 +1,31 @@ +import { State } from "@port-of-mars/shared/game/client/state"; +import Getters from "@port-of-mars/client/store/getters"; +import Mutations from "@port-of-mars/client/store/mutations"; +import { VueConstructor } from "vue"; +import { Store } from "vuex"; + +export interface TStore { + state: State; + readonly getters: { [K in keyof typeof Getters]: ReturnType<(typeof Getters)[K]> }; + + commit( + name: K, + payload?: Parameters<(typeof Mutations)[K]>[1] + ): void; +} + +declare module "vue/types/vue" { + interface Vue { + $tstore: TStore; + } +} + +export const TypedStore = { + install(instance: VueConstructor) { + Object.defineProperty(instance.prototype, "$tstore", { + get: function (this: { $store: Store }) { + return this.$store; + }, + }); + }, +}; diff --git a/client/src/util.ts b/client/src/util.ts index 3304943cb..76f395b4b 100644 --- a/client/src/util.ts +++ b/client/src/util.ts @@ -1,10 +1,11 @@ import { SetSfx } from "@port-of-mars/shared/game/responses"; import { Sfx } from "@port-of-mars/shared/game/responses"; +import { SERVER_URL_HTTP } from "@port-of-mars/shared/settings"; import { Howl } from "howler"; export function url(path: string) { // workaround to connect to localhost:2567 server endpoints - return `${process.env.SERVER_URL_HTTP}${path}`; + return `${SERVER_URL_HTTP}${path}`; } export function getAssetUrl(path: string) { diff --git a/client/src/views/FreePlayLobby.vue b/client/src/views/FreePlayLobby.vue index eb00f2708..6957026f4 100644 --- a/client/src/views/FreePlayLobby.vue +++ b/client/src/views/FreePlayLobby.vue @@ -26,7 +26,6 @@ import { FreePlayLobbyRequestAPI } from "@port-of-mars/client/api/lobby/request" import { AccountAPI } from "@port-of-mars/client/api/account/request"; import { FREE_PLAY_LOBBY_NAME } from "@port-of-mars/shared/lobby"; import { GAME_PAGE, CONSENT_PAGE, MANUAL_PAGE } from "@port-of-mars/shared/routes"; -import { Constants } from "@port-of-mars/shared/settings"; import Countdown from "@port-of-mars/client/components/global/Countdown.vue"; import HelpPanel from "@port-of-mars/client/components/lobby/HelpPanel.vue"; import Messages from "@port-of-mars/client/components/global/Messages.vue"; @@ -47,10 +46,6 @@ export default class FreePlayLobby extends Vue { manual = { name: MANUAL_PAGE }; consent = { name: CONSENT_PAGE }; - get constants() { - return Constants; - } - async created() { this.accountApi = new AccountAPI(this.$store, this.$ajax); await this.checkCanPlay(); diff --git a/client/src/views/Home.vue b/client/src/views/Home.vue index 996a7c482..a2697a80d 100644 --- a/client/src/views/Home.vue +++ b/client/src/views/Home.vue @@ -78,7 +78,7 @@ class="p-1" type="iframe" aspect="16by9" - :src="constants.TUTORIAL_VIDEO_URL" + :src="$settings.TUTORIAL_VIDEO_URL" allowfullscreen > @@ -88,7 +88,7 @@ class="p-1" type="iframe" aspect="16by9" - :src="constants.TRAILER_VIDEO_URL" + :src="$settings.TRAILER_VIDEO_URL" allowfullscreen > @@ -117,7 +117,7 @@

Community

Discuss the game, find a game to play, or connect with other players in our - community Discord. + community Discord.

Keep track of your performance with your @@ -155,7 +155,6 @@ import { TOURNAMENT_DASHBOARD_PAGE, MANUAL_PAGE, } from "@port-of-mars/shared/routes"; -import { Constants } from "@port-of-mars/shared/settings"; import Footer from "@port-of-mars/client/components/global/Footer.vue"; import CharCarousel from "@port-of-mars/client/components/global/CharCarousel.vue"; import AgeTooltip from "@port-of-mars/client/components/global/AgeTooltip.vue"; @@ -185,10 +184,6 @@ export default class Home extends Vue { solo = { name: SOLO_GAME_PAGE }; manual = { name: MANUAL_PAGE }; - get constants() { - return Constants; - } - get shouldShowTournamentBanner() { return this.$tstore.state.isTournamentEnabled && this.$tstore.getters.tournamentStatus; } diff --git a/client/src/views/Privacy.vue b/client/src/views/Privacy.vue index 186b5b6fb..824f5d25b 100644 --- a/client/src/views/Privacy.vue +++ b/client/src/views/Privacy.vue @@ -78,7 +78,7 @@

If you have any questions or concerns about our Privacy Policy or the collection, use, or sharing of your personal information, please contact us at - {{ constants.CONTACT_EMAIL }}{{ $settings.CONTACT_EMAIL }}.

@@ -88,7 +88,6 @@